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:
+2
-2
@@ -197,7 +197,7 @@ func run() error {
|
|||||||
Motivation: deps.Motivation, Narrative: deps.Narrative, Notifications: deps.Notifications})
|
Motivation: deps.Motivation, Narrative: deps.Narrative, Notifications: deps.Notifications})
|
||||||
brainCfg := brain.Config{DB: db, Events: deps.Events, Minds: deps.Minds, Registry: registry, Pool: scheduler.NewPool(cfg.MaxWorkers),
|
brainCfg := brain.Config{DB: db, Events: deps.Events, Minds: deps.Minds, Registry: registry, Pool: scheduler.NewPool(cfg.MaxWorkers),
|
||||||
Log: log, Metrics: metrics, Services: brain.Services{Events: deps.Events, Memory: deps.Memory, Beliefs: deps.Beliefs, LLM: deps.LLM, Prompts: deps.Prompts,
|
Log: log, Metrics: metrics, Services: brain.Services{Events: deps.Events, Memory: deps.Memory, Beliefs: deps.Beliefs, LLM: deps.LLM, Prompts: deps.Prompts,
|
||||||
Goals: deps.Goals, Motivation: deps.Motivation, Predictions: deps.Predictions, Capabilities: deps.Capabilities, Volition: deps.Volition, Plans: deps.Plans, Executor: deps.Executor},
|
Goals: deps.Goals, Motivation: deps.Motivation, Predictions: deps.Predictions, Capabilities: deps.Capabilities, Volition: deps.Volition, Plans: deps.Plans, Executor: deps.Executor, Minds: deps.Minds},
|
||||||
AfterCommit: deps.Executor.WakeOn()}
|
AfterCommit: deps.Executor.WakeOn()}
|
||||||
deps.Brain = brain.NewManager(brainCfg, group)
|
deps.Brain = brain.NewManager(brainCfg, group)
|
||||||
// Sandbox runtimes (replay, experiments) share the registry but use a
|
// Sandbox runtimes (replay, experiments) share the registry but use a
|
||||||
@@ -214,7 +214,7 @@ func run() error {
|
|||||||
deps.Snapshots = &snapshots.Store{DB: db, Events: deps.Events, Minds: deps.Minds, Identity: ident}
|
deps.Snapshots = &snapshots.Store{DB: db, Events: deps.Events, Minds: deps.Minds, Identity: ident}
|
||||||
deps.Privacy = &privacy.Service{DB: db, Events: deps.Events, Audit: auditLog, Snapshots: deps.Snapshots, Log: log}
|
deps.Privacy = &privacy.Service{DB: db, Events: deps.Events, Audit: auditLog, Snapshots: deps.Snapshots, Log: log}
|
||||||
deps.Replay = &replay.Service{DB: db, Events: deps.Events, Minds: deps.Minds, Snapshots: deps.Snapshots, Brain: sandboxCfg, Identity: ident, Log: log}
|
deps.Replay = &replay.Service{DB: db, Events: deps.Events, Minds: deps.Minds, Snapshots: deps.Snapshots, Brain: sandboxCfg, Identity: ident, Log: log}
|
||||||
deps.Experiments = &experiments.Service{DB: db, Minds: deps.Minds, Snapshots: deps.Snapshots, Brain: sandboxCfg, Identity: ident, Log: log, Routing: func(ctx context.Context) []map[string]any {
|
deps.Experiments = &experiments.Service{DB: db, Minds: deps.Minds, Snapshots: deps.Snapshots, Brain: sandboxCfg, Identity: ident, Log: log, Capabilities: deps.Capabilities, Routing: func(ctx context.Context) []map[string]any {
|
||||||
roles, _ := deps.LLMStore.ListRoles(ctx)
|
roles, _ := deps.LLMStore.ListRoles(ctx)
|
||||||
b, _ := json.Marshal(roles)
|
b, _ := json.Marshal(roles)
|
||||||
var out []map[string]any
|
var out []map[string]any
|
||||||
|
|||||||
+85
-8
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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 {
|
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))
|
motivation.Decay(&ms, motivation.Baselines(r.mind.Personality), start.Sub(ms.Affect.UpdatedAt))
|
||||||
st.Motiv = ms
|
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}
|
c := &Cycle{Runtime: r, State: st, Tx: tx}
|
||||||
|
|
||||||
// 1. Ask every process, concurrently and bounded.
|
// 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
|
byID[cand.ID] = cand
|
||||||
b := attention.Score(cand.Features, weights)
|
b := attention.Score(cand.Features, weights)
|
||||||
fp := fingerprint(*cand)
|
fp := fingerprint(*cand)
|
||||||
if r.loops.looping(fp) {
|
if fate, match := r.override(ctx, *cand); fate == "exclude" {
|
||||||
suppressed[cand.ID] = fp
|
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
|
b.Total = 0
|
||||||
if r.loops.shouldWarn(fp) {
|
if r.loops.shouldWarn(fp) {
|
||||||
c.Emit(events.New{Type: events.Metacognition, Source: "brain.loop-detector", Summary: "suppressed recurring thought without progress: " + cand.Summary,
|
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]
|
o, has := outcomeByID[cand.ID]
|
||||||
fate := "rejected"
|
fate := "rejected"
|
||||||
reason := o.Reason
|
reason := o.Reason
|
||||||
if fp, s := suppressed[cand.ID]; s {
|
if why, s := suppressed[cand.ID]; s {
|
||||||
fate, reason = "suppressed", "loop detector: "+fp
|
fate, reason = "suppressed", why
|
||||||
|
if strings.HasPrefix(why, "experimental") {
|
||||||
|
fate = "excluded"
|
||||||
|
}
|
||||||
} else if has && o.Admitted {
|
} else if has && o.Admitted {
|
||||||
fate = "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}))
|
r.loops.record(fingerprint(Candidate{Process: t.Process, Kind: t.Kind, Subject: t.Subject}))
|
||||||
}
|
}
|
||||||
for _, h := range r.registry.hooks {
|
for _, h := range r.registry.hooks {
|
||||||
|
if r.ablatedHook(h.Name()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := h.AfterCycle(ctx, c, c.emitted); err != nil {
|
if err := h.AfterCycle(ctx, c, c.emitted); err != nil {
|
||||||
r.log.Warn("hook failed", "hook", h.Name(), "err", err)
|
r.log.Warn("hook failed", "hook", h.Name(), "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pruned := r.state.Prune(0.05)
|
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.
|
// 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,
|
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}})
|
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)),
|
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)},
|
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}})
|
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
|
// observe runs all processes through the bounded pool with per-process
|
||||||
// timeouts and isolation. A failing process yields no candidates.
|
// timeouts and isolation. A failing process yields no candidates.
|
||||||
func (r *Runtime) observe(ctx context.Context, st *State) []Candidate {
|
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))
|
results := make([][]Candidate, len(procs))
|
||||||
tasks := make([]func(context.Context) error, len(procs))
|
tasks := make([]func(context.Context) error, len(procs))
|
||||||
for i, p := range procs {
|
for i, p := range procs {
|
||||||
@@ -744,3 +775,49 @@ func (b *cycleBudget) allow(now time.Time) bool {
|
|||||||
b.tokens--
|
b.tokens--
|
||||||
return true
|
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
|
Predictions *predictions.Store
|
||||||
Capabilities *capabilities.Service
|
Capabilities *capabilities.Service
|
||||||
Volition *volition.Store
|
Volition *volition.Store
|
||||||
|
Minds *minds.Store // self-model corrections
|
||||||
Plans *planning.Store
|
Plans *planning.Store
|
||||||
// Executor plans formed intentions inside the cycle transaction. It never
|
// Executor plans formed intentions inside the cycle transaction. It never
|
||||||
// acts from inside a cycle: acting happens after commit, in its own loop.
|
// acts from inside a cycle: acting happens after commit, in its own loop.
|
||||||
|
|||||||
@@ -308,6 +308,11 @@ func (h Handlers) intend(ctx context.Context, c *brain.Cycle, t workspace.Though
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
in.Action = ActionInfo(ctx, s.Services.Capabilities, s.Mind.ID, w.Proposal)
|
in.Action = ActionInfo(ctx, s.Services.Capabilities, s.Mind.ID, w.Proposal)
|
||||||
|
if in.Action != nil {
|
||||||
|
if err := correctSelfModel(ctx, c, *in.Action, t.EventID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if s.Services.Capabilities != nil {
|
if s.Services.Capabilities != nil {
|
||||||
if inv, err := s.Services.Capabilities.Inventory(ctx, s.Mind.ID); err == nil {
|
if inv, err := s.Services.Capabilities.Inventory(ctx, s.Mind.ID); err == nil {
|
||||||
for _, i := range inv {
|
for _, i := range inv {
|
||||||
@@ -431,3 +436,44 @@ func (h Handlers) want(ctx context.Context, c *brain.Cycle, t workspace.Thought)
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// correctSelfModel compares what the self-model says about a capability
|
||||||
|
// with what policy actually gives now. A mismatch is recorded as a
|
||||||
|
// metacognitive finding and the self-model entry is corrected: reality, as
|
||||||
|
// the capability layer reports it, wins over the mind's belief about itself.
|
||||||
|
func correctSelfModel(ctx context.Context, c *brain.Cycle, a volition.ActionInfo, cause ids.ID) error {
|
||||||
|
s := c.State
|
||||||
|
if s.Services.Minds == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
actual := a.Effect
|
||||||
|
if actual == "locked" || actual == "unknown" {
|
||||||
|
actual = "unavailable"
|
||||||
|
}
|
||||||
|
for i, entry := range s.SelfModel.Capabilities {
|
||||||
|
if entry.Name != a.Capability || entry.Policy == actual {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ev, err := c.EmitNow(ctx, events.New{Type: events.Metacognition, Source: "brain.selfmodel", Summary: fmt.Sprintf("self-model said %s is %s, but policy gives %s; corrected", a.Capability, entry.Policy, actual),
|
||||||
|
Salience: 0.6, Confidence: 1, CausationID: cause, Metadata: map[string]any{"check": "self_model_mismatch", "ref": a.Capability, "stated": entry.Policy, "actual": actual}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sm, err := s.Services.Minds.GetSelfModel(ctx, s.Mind.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for j := range sm.Capabilities {
|
||||||
|
if sm.Capabilities[j].Name == a.Capability {
|
||||||
|
sm.Capabilities[j].Policy = actual
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Services.Minds.SaveSelfModel(ctx, s.Mind.ID, sm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.SelfModel.Capabilities[i].Policy = actual
|
||||||
|
c.Emit(events.New{Type: events.SelfModelUpdated, Source: "brain.selfmodel", Summary: "self-model corrected: " + a.Capability + " is " + actual, Salience: 0.4, Confidence: 1, CausationID: ev.ID,
|
||||||
|
Metadata: map[string]any{"correction": true, "capability": a.Capability, "from": entry.Policy, "to": actual}})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
"github.com/andydixon/noema/internal/brain"
|
"github.com/andydixon/noema/internal/brain"
|
||||||
|
"github.com/andydixon/noema/internal/capabilities"
|
||||||
|
evstore "github.com/andydixon/noema/internal/events"
|
||||||
"github.com/andydixon/noema/internal/identity"
|
"github.com/andydixon/noema/internal/identity"
|
||||||
"github.com/andydixon/noema/internal/minds"
|
"github.com/andydixon/noema/internal/minds"
|
||||||
"github.com/andydixon/noema/internal/replay"
|
"github.com/andydixon/noema/internal/replay"
|
||||||
|
"github.com/andydixon/noema/internal/research"
|
||||||
"github.com/andydixon/noema/internal/snapshots"
|
"github.com/andydixon/noema/internal/snapshots"
|
||||||
"github.com/andydixon/noema/internal/storage"
|
"github.com/andydixon/noema/internal/storage"
|
||||||
"github.com/andydixon/noema/pkg/events"
|
"github.com/andydixon/noema/pkg/events"
|
||||||
@@ -38,6 +41,50 @@ type Definition struct {
|
|||||||
// each with its own overrides (typically a different model per role).
|
// each with its own overrides (typically a different model per role).
|
||||||
// When empty the definition's own overrides form a single subject.
|
// When empty the definition's own overrides form a single subject.
|
||||||
Variants []Variant `json:"variants,omitempty"`
|
Variants []Variant `json:"variants,omitempty"`
|
||||||
|
// Protocol states what the experiment tests and how
|
||||||
|
// (docs/cognitive-research.md).
|
||||||
|
Protocol *Protocol `json:"protocol,omitempty"`
|
||||||
|
// Replay feeds recorded stimuli from a mind's history instead of
|
||||||
|
// scripted text (counterfactual experiments omit some of them).
|
||||||
|
Replay *ReplaySpec `json:"replay,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol kinds.
|
||||||
|
const (
|
||||||
|
ProtocolCustom = "custom"
|
||||||
|
ProtocolGlobalWorkspace = "global_workspace"
|
||||||
|
ProtocolAblation = "ablation"
|
||||||
|
ProtocolSelfModel = "self_model"
|
||||||
|
ProtocolCounterfactual = "counterfactual"
|
||||||
|
ProtocolModelComparison = "model_comparison"
|
||||||
|
ProtocolConflict = "conflict"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Protocol records the design of an experiment.
|
||||||
|
type Protocol struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
IndependentVariable string `json:"independent_variable"`
|
||||||
|
ControlledVariables []string `json:"controlled_variables"`
|
||||||
|
Measures []string `json:"measures"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaySpec selects recorded stimuli: perception and operator events of a
|
||||||
|
// mind in (from_seq, to_seq]. Omit lists events a counterfactual variant
|
||||||
|
// leaves out.
|
||||||
|
type ReplaySpec struct {
|
||||||
|
MindID ids.ID `json:"mind_id"`
|
||||||
|
FromSeq int64 `json:"from_seq"`
|
||||||
|
ToSeq int64 `json:"to_seq"`
|
||||||
|
Omit []ids.ID `json:"omit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfModelOverride replaces parts of a fork's self-model. It changes what
|
||||||
|
// the mind believes about itself, never what policy allows.
|
||||||
|
type SelfModelOverride struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Capabilities []minds.CapabilityEntry `json:"capabilities,omitempty"`
|
||||||
|
KnowledgeGaps []string `json:"knowledge_gaps,omitempty"`
|
||||||
|
CurrentGoals []string `json:"current_goals,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variant is one fork's overrides in a comparative experiment.
|
// Variant is one fork's overrides in a comparative experiment.
|
||||||
@@ -46,6 +93,13 @@ type Variant struct {
|
|||||||
Config map[string]any `json:"config,omitempty"`
|
Config map[string]any `json:"config,omitempty"`
|
||||||
Personality map[string]any `json:"personality,omitempty"`
|
Personality map[string]any `json:"personality,omitempty"`
|
||||||
LLMRoles map[string]string `json:"llm_roles,omitempty"` // role → model id, resolved by the router for this fork only
|
LLMRoles map[string]string `json:"llm_roles,omitempty"` // role → model id, resolved by the router for this fork only
|
||||||
|
// Ablations and WorkspaceOverrides are research manipulations applied
|
||||||
|
// to this fork only (minds.Ablations lists the vocabulary).
|
||||||
|
Ablations []string `json:"ablations,omitempty"`
|
||||||
|
WorkspaceOverrides []minds.WorkspaceOverride `json:"workspace_overrides,omitempty"`
|
||||||
|
SelfModel *SelfModelOverride `json:"self_model,omitempty"`
|
||||||
|
// Omit leaves recorded stimuli out of this variant (counterfactuals).
|
||||||
|
Omit []ids.ID `json:"omit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stimulus is one scripted observation.
|
// Stimulus is one scripted observation.
|
||||||
@@ -98,6 +152,28 @@ type Results struct {
|
|||||||
// Comparative runs: one entry per variant and a deterministic comparison.
|
// Comparative runs: one entry per variant and a deterministic comparison.
|
||||||
Variants []VariantResult `json:"variants,omitempty"`
|
Variants []VariantResult `json:"variants,omitempty"`
|
||||||
Comparison *Comparison `json:"comparison,omitempty"`
|
Comparison *Comparison `json:"comparison,omitempty"`
|
||||||
|
// Protocol echoes the design; Reproducibility compares with the previous
|
||||||
|
// run of the same definition; Counterfactual reports what omitting
|
||||||
|
// events changed.
|
||||||
|
Protocol *Protocol `json:"protocol,omitempty"`
|
||||||
|
Reproducibility Reproducibility `json:"reproducibility"`
|
||||||
|
Counterfactual *CounterfactualReport `json:"counterfactual,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproducibility labels a run against the previous run of the same
|
||||||
|
// definition.
|
||||||
|
type Reproducibility struct {
|
||||||
|
Status string `json:"status"` // single run | reproduced | diverged | nondeterministic substrate
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
PreviousDigest string `json:"previous_digest,omitempty"`
|
||||||
|
Note string `json:"note"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CounterfactualReport is what a counterfactual experiment found.
|
||||||
|
type CounterfactualReport struct {
|
||||||
|
Omitted []ids.ID `json:"omitted"`
|
||||||
|
OmittedSummaries []string `json:"omitted_summaries"`
|
||||||
|
Findings []string `json:"findings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VariantResult is what one fork produced.
|
// VariantResult is what one fork produced.
|
||||||
@@ -119,6 +195,24 @@ type VariantResult struct {
|
|||||||
Cycles int `json:"cycles"`
|
Cycles int `json:"cycles"`
|
||||||
TruncatedAfter int `json:"truncated_after_stimulus,omitempty"`
|
TruncatedAfter int `json:"truncated_after_stimulus,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
// Research measures of the fork after the stimuli.
|
||||||
|
Ablations []string `json:"ablations,omitempty"`
|
||||||
|
Wants []string `json:"wants"`
|
||||||
|
Intentions []string `json:"intentions"`
|
||||||
|
IntentionsByStatus map[string]int `json:"intentions_by_status"`
|
||||||
|
Commitments int `json:"commitments"`
|
||||||
|
Conflicts int `json:"want_conflicts"`
|
||||||
|
Plans int `json:"plans"`
|
||||||
|
PlanSteps int `json:"plan_steps"`
|
||||||
|
PlanCapabilities []string `json:"plan_capabilities"`
|
||||||
|
Downstream map[string]int `json:"downstream_by_category"`
|
||||||
|
IntrospectionAccuracy *float64 `json:"introspection_accuracy,omitempty"`
|
||||||
|
SelfModelAccuracy *float64 `json:"self_model_accuracy,omitempty"`
|
||||||
|
SelfModelErrors []string `json:"self_model_errors,omitempty"`
|
||||||
|
CorrectionLatency int `json:"self_model_correction_latency"`
|
||||||
|
Measures map[string]float64 `json:"measures"`
|
||||||
|
EventRange [2]int64 `json:"event_range"`
|
||||||
|
Omitted []ids.ID `json:"omitted,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comparison is the deterministic divergence report across variants. All
|
// Comparison is the deterministic divergence report across variants. All
|
||||||
@@ -133,6 +227,13 @@ type Comparison struct {
|
|||||||
CostRange [2]float64 `json:"cost_range"`
|
CostRange [2]float64 `json:"cost_range"`
|
||||||
CapabilityRequestRange [2]int `json:"capability_request_range"`
|
CapabilityRequestRange [2]int `json:"capability_request_range"`
|
||||||
Notes []string `json:"notes"`
|
Notes []string `json:"notes"`
|
||||||
|
// Volition, planning and research comparisons.
|
||||||
|
WantOverlap float64 `json:"want_overlap"`
|
||||||
|
IntentionOverlap float64 `json:"intention_overlap"`
|
||||||
|
PlanCapabilityOverlap float64 `json:"plan_capability_overlap"`
|
||||||
|
MeasureRanges map[string][2]float64 `json:"measure_ranges"`
|
||||||
|
ArchitectureShare float64 `json:"architecture_share"`
|
||||||
|
DownstreamDelta map[string]int `json:"downstream_delta,omitempty"` // first variant minus second
|
||||||
}
|
}
|
||||||
|
|
||||||
// StepResult records one stimulus.
|
// StepResult records one stimulus.
|
||||||
@@ -168,6 +269,9 @@ type Service struct {
|
|||||||
Routing func(ctx context.Context) []map[string]any // current LLM role routing, recorded with results
|
Routing func(ctx context.Context) []map[string]any // current LLM role routing, recorded with results
|
||||||
Identity *identity.Capturer
|
Identity *identity.Capturer
|
||||||
Log *slog.Logger
|
Log *slog.Logger
|
||||||
|
// Capabilities, when set, lets self-model experiments compare the
|
||||||
|
// self-model with effective policy. It is never used to act.
|
||||||
|
Capabilities *capabilities.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrBusy is returned when a run is already in progress.
|
// ErrBusy is returned when a run is already in progress.
|
||||||
@@ -175,6 +279,41 @@ var ErrBusy = errors.New("experiment already running")
|
|||||||
|
|
||||||
// Validate bounds a definition.
|
// Validate bounds a definition.
|
||||||
func (d *Definition) Validate() error {
|
func (d *Definition) Validate() error {
|
||||||
|
if d.Protocol != nil {
|
||||||
|
switch d.Protocol.Kind {
|
||||||
|
case "", ProtocolCustom, ProtocolGlobalWorkspace, ProtocolAblation, ProtocolSelfModel, ProtocolCounterfactual, ProtocolModelComparison, ProtocolConflict:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown protocol kind %q", d.Protocol.Kind)
|
||||||
|
}
|
||||||
|
for _, m := range d.Protocol.Measures {
|
||||||
|
if _, ok := research.MeasureByName(m); !ok {
|
||||||
|
return fmt.Errorf("unknown measure %q", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.Protocol.Kind == ProtocolCounterfactual {
|
||||||
|
if d.Replay == nil || len(d.Replay.Omit) == 0 {
|
||||||
|
return errors.New("a counterfactual experiment replays recorded stimuli and omits at least one event")
|
||||||
|
}
|
||||||
|
if len(d.Variants) == 0 {
|
||||||
|
d.Variants = []Variant{{Name: "factual"}, {Name: "counterfactual", Omit: d.Replay.Omit}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range d.Variants {
|
||||||
|
cfg := minds.CognitiveConfig{Ablations: v.Ablations, WorkspaceOverrides: v.WorkspaceOverrides}
|
||||||
|
if err := cfg.ValidateResearch(); err != nil {
|
||||||
|
return fmt.Errorf("variant %s: %w", v.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.Replay != nil {
|
||||||
|
if d.Replay.MindID.IsZero() {
|
||||||
|
return errors.New("replay needs the source mind")
|
||||||
|
}
|
||||||
|
if d.Duration <= 0 || d.Duration > 3600 {
|
||||||
|
d.Duration = 300
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if len(d.Stimuli) == 0 {
|
if len(d.Stimuli) == 0 {
|
||||||
return errors.New("at least one stimulus is required")
|
return errors.New("at least one stimulus is required")
|
||||||
}
|
}
|
||||||
@@ -248,11 +387,16 @@ func (s *Service) Run(ctx context.Context, id ids.ID, by string) (Experiment, er
|
|||||||
|
|
||||||
func (s *Service) execute(ctx context.Context, e *Experiment, by string) (Results, error) {
|
func (s *Service) execute(ctx context.Context, e *Experiment, by string) (Results, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
res := Results{EventsByType: map[string]int{}, LLMCalls: map[string]int{}}
|
res := Results{EventsByType: map[string]int{}, LLMCalls: map[string]int{}, Protocol: e.Definition.Protocol}
|
||||||
|
previous := ResultsOf(*e).Reproducibility.Digest
|
||||||
a, err := s.Snapshots.Archive(ctx, e.SnapshotID)
|
a, err := s.Snapshots.Archive(ctx, e.SnapshotID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
stimuli, err := s.stimuli(ctx, e, a)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
variants := e.Definition.Variants
|
variants := e.Definition.Variants
|
||||||
if len(variants) == 0 {
|
if len(variants) == 0 {
|
||||||
variants = []Variant{{Name: "subject", Config: e.Definition.Config, Personality: e.Definition.Personality}}
|
variants = []Variant{{Name: "subject", Config: e.Definition.Config, Personality: e.Definition.Personality}}
|
||||||
@@ -262,7 +406,7 @@ func (s *Service) execute(ctx context.Context, e *Experiment, by string) (Result
|
|||||||
}
|
}
|
||||||
deadline := start.Add(time.Duration(e.Definition.Duration) * time.Second)
|
deadline := start.Add(time.Duration(e.Definition.Duration) * time.Second)
|
||||||
for i, v := range variants {
|
for i, v := range variants {
|
||||||
vr, subject, err := s.runVariant(ctx, e, a, i, v, by, deadline)
|
vr, subject, err := s.runVariant(ctx, e, a, i, v, by, deadline, stimuli)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
return res, err
|
return res, err
|
||||||
@@ -281,16 +425,70 @@ func (s *Service) execute(ctx context.Context, e *Experiment, by string) (Result
|
|||||||
if len(res.Variants) > 1 {
|
if len(res.Variants) > 1 {
|
||||||
res.Comparison = Compare(res.Variants)
|
res.Comparison = Compare(res.Variants)
|
||||||
}
|
}
|
||||||
|
res.Reproducibility = reproducibility(res.Variants, previous)
|
||||||
|
if e.Definition.Protocol != nil && e.Definition.Protocol.Kind == ProtocolCounterfactual && len(res.Variants) >= 2 {
|
||||||
|
res.Counterfactual = counterfactualReport(res.Variants[0], res.Variants[1], stimuli)
|
||||||
|
}
|
||||||
res.DurationMS = time.Since(start).Milliseconds()
|
res.DurationMS = time.Since(start).Milliseconds()
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// input is one stimulus to feed: scripted text or a recorded event.
|
||||||
|
type input struct {
|
||||||
|
Text string
|
||||||
|
Source string
|
||||||
|
Salience float64
|
||||||
|
Recorded *events.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// stimuli resolves the definition's inputs. Recorded stimuli must come from
|
||||||
|
// the snapshot's own mind and lie after the snapshot.
|
||||||
|
func (s *Service) stimuli(ctx context.Context, e *Experiment, a snapshots.Archive) ([]input, error) {
|
||||||
|
d := e.Definition
|
||||||
|
if d.Replay == nil {
|
||||||
|
out := make([]input, 0, len(d.Stimuli))
|
||||||
|
for _, st := range d.Stimuli {
|
||||||
|
out = append(out, input{Text: st.Text, Source: st.Source, Salience: st.Salience})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
if d.Replay.MindID != a.Source.MindID {
|
||||||
|
return nil, errors.New("recorded stimuli must come from the mind the snapshot was taken of")
|
||||||
|
}
|
||||||
|
from := d.Replay.FromSeq
|
||||||
|
if from < a.Source.EventSeq {
|
||||||
|
from = a.Source.EventSeq
|
||||||
|
}
|
||||||
|
to := d.Replay.ToSeq
|
||||||
|
if to <= from {
|
||||||
|
to = 1<<62 - 1
|
||||||
|
}
|
||||||
|
evs, err := s.Snapshots.Events.Query(ctx, evstore.Filter{MindID: d.Replay.MindID, Categories: replay.StimulusCategories, AfterSeq: from, BeforeSeq: to + 1, Ascending: true, Limit: replay.MaxStimuli + 1})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var out []input
|
||||||
|
for i := range evs {
|
||||||
|
if replay.IsStimulus(evs[i]) && evs[i].RedactedAt == nil {
|
||||||
|
ev := evs[i]
|
||||||
|
out = append(out, input{Text: ev.Summary, Source: ev.Source, Salience: ev.Salience, Recorded: &ev})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil, errors.New("no recorded stimuli after the snapshot in that range")
|
||||||
|
}
|
||||||
|
if len(out) > replay.MaxStimuli {
|
||||||
|
return nil, fmt.Errorf("more than %d stimuli; narrow the range", replay.MaxStimuli)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// runVariant forks the snapshot with one variant's overrides, feeds every
|
// runVariant forks the snapshot with one variant's overrides, feeds every
|
||||||
// stimulus and measures the outcome. Per-mind model routing lives in the
|
// stimulus and measures the outcome. Per-mind model routing lives in the
|
||||||
// fork's configuration, so the router applies it without any change to
|
// fork's configuration, so the router applies it without any change to
|
||||||
// callers, and the identity records it.
|
// callers, and the identity records it.
|
||||||
func (s *Service) runVariant(ctx context.Context, e *Experiment, a snapshots.Archive, index int, v Variant, by string, deadline time.Time) (VariantResult, minds.Mind, error) {
|
func (s *Service) runVariant(ctx context.Context, e *Experiment, a snapshots.Archive, index int, v Variant, by string, deadline time.Time, stimuli []input) (VariantResult, minds.Mind, error) {
|
||||||
vr := VariantResult{Name: firstNonEmpty(v.Name, fmt.Sprintf("variant-%d", index+1)), EventsByType: map[string]int{}, LLMCalls: map[string]int{}}
|
vr := VariantResult{Name: firstNonEmpty(v.Name, fmt.Sprintf("variant-%d", index+1)), EventsByType: map[string]int{}, LLMCalls: map[string]int{}, Ablations: v.Ablations, Omitted: v.Omit, CorrectionLatency: -1}
|
||||||
cfg, err := override(a.Mind.Config, v.Config)
|
cfg, err := override(a.Mind.Config, v.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vr, minds.Mind{}, fmt.Errorf("%s: config overrides: %w", vr.Name, err)
|
return vr, minds.Mind{}, fmt.Errorf("%s: config overrides: %w", vr.Name, err)
|
||||||
@@ -298,6 +496,12 @@ func (s *Service) runVariant(ctx context.Context, e *Experiment, a snapshots.Arc
|
|||||||
if len(v.LLMRoles) > 0 {
|
if len(v.LLMRoles) > 0 {
|
||||||
cfg.LLMRoles = v.LLMRoles
|
cfg.LLMRoles = v.LLMRoles
|
||||||
}
|
}
|
||||||
|
if len(v.Ablations) > 0 {
|
||||||
|
cfg.Ablations = v.Ablations
|
||||||
|
}
|
||||||
|
if len(v.WorkspaceOverrides) > 0 {
|
||||||
|
cfg.WorkspaceOverrides = v.WorkspaceOverrides
|
||||||
|
}
|
||||||
pers, err := override(a.Mind.Personality, v.Personality)
|
pers, err := override(a.Mind.Personality, v.Personality)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vr, minds.Mind{}, fmt.Errorf("%s: personality overrides: %w", vr.Name, err)
|
return vr, minds.Mind{}, fmt.Errorf("%s: personality overrides: %w", vr.Name, err)
|
||||||
@@ -312,27 +516,58 @@ func (s *Service) runVariant(ctx context.Context, e *Experiment, a snapshots.Arc
|
|||||||
return vr, minds.Mind{}, fmt.Errorf("fork %s: %w", vr.Name, err)
|
return vr, minds.Mind{}, fmt.Errorf("fork %s: %w", vr.Name, err)
|
||||||
}
|
}
|
||||||
vr.Subject, vr.SubjectSlug = subject.ID, subject.Slug
|
vr.Subject, vr.SubjectSlug = subject.ID, subject.Slug
|
||||||
vr.Routing = s.Identity.Capture(ctx, subject.ID).Routing
|
if s.Identity != nil {
|
||||||
|
vr.Routing = s.Identity.Capture(ctx, subject.ID).Routing
|
||||||
|
}
|
||||||
|
if v.SelfModel != nil {
|
||||||
|
if err := s.applySelfModel(ctx, subject, *v.SelfModel); err != nil {
|
||||||
|
return vr, subject, fmt.Errorf("%s: self-model override: %w", vr.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vr.SelfModelAccuracy, vr.SelfModelErrors = s.selfModelAccuracy(ctx, subject)
|
||||||
drv, err := replay.NewDriver(ctx, s.Brain, subject, "experiment")
|
drv, err := replay.NewDriver(ctx, s.Brain, subject, "experiment")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vr, subject, err
|
return vr, subject, err
|
||||||
}
|
}
|
||||||
var all []events.Event
|
var all []events.Event
|
||||||
for i, st := range e.Definition.Stimuli {
|
omit := map[ids.ID]bool{}
|
||||||
|
for _, id := range v.Omit {
|
||||||
|
omit[id] = true
|
||||||
|
}
|
||||||
|
var injected []ids.ID
|
||||||
|
for i, st := range stimuli {
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
vr.TruncatedAfter = i
|
vr.TruncatedAfter = i
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
ev, err := drv.Observe(ctx, st.Source, st.Text, st.Salience, map[string]any{"experiment_id": e.ID, "variant": vr.Name, "stimulus": i, "seed": e.Definition.Seed})
|
tag := map[string]any{"experiment_id": e.ID, "variant": vr.Name, "stimulus": i, "seed": e.Definition.Seed}
|
||||||
|
var ev events.Event
|
||||||
|
switch {
|
||||||
|
case st.Recorded != nil && omit[st.Recorded.ID]:
|
||||||
|
// The counterfactual: this event never happens in this variant.
|
||||||
|
vr.Steps = append(vr.Steps, StepResult{Index: i, Text: "(omitted) " + st.Text, Produced: []string{}})
|
||||||
|
continue
|
||||||
|
case st.Recorded != nil:
|
||||||
|
tag["replay_of"] = st.Recorded.ID
|
||||||
|
ev, err = drv.Inject(ctx, *st.Recorded, tag)
|
||||||
|
default:
|
||||||
|
ev, err = drv.Observe(ctx, st.Source, st.Text, st.Salience, tag)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vr, subject, err
|
return vr, subject, err
|
||||||
}
|
}
|
||||||
|
injected = append(injected, ev.ID)
|
||||||
produced, cycles, err := drv.Settle(ctx)
|
produced, cycles, err := drv.Settle(ctx)
|
||||||
vr.Cycles += cycles
|
vr.Cycles += cycles
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Warn("experiment cycle error", "experiment", e.ID, "variant", vr.Name, "err", err)
|
s.Log.Warn("experiment cycle error", "experiment", e.ID, "variant", vr.Name, "err", err)
|
||||||
}
|
}
|
||||||
all = append(all, produced...)
|
all = append(all, produced...)
|
||||||
|
for _, p := range produced {
|
||||||
|
if vr.CorrectionLatency < 0 && p.Type == events.SelfModelUpdated && p.Metadata["correction"] == true {
|
||||||
|
vr.CorrectionLatency = i
|
||||||
|
}
|
||||||
|
}
|
||||||
vr.Steps = append(vr.Steps, StepResult{Index: i, Text: st.Text, EventID: ev.ID, Produced: replay.Digest(produced), Cycles: cycles})
|
vr.Steps = append(vr.Steps, StepResult{Index: i, Text: st.Text, EventID: ev.ID, Produced: replay.Digest(produced), Cycles: cycles})
|
||||||
}
|
}
|
||||||
for _, ev := range all {
|
for _, ev := range all {
|
||||||
@@ -370,6 +605,10 @@ func (s *Service) runVariant(ctx context.Context, e *Experiment, a snapshots.Arc
|
|||||||
var cost float64
|
var cost float64
|
||||||
s.DB.QueryRow(ctx, `SELECT count(*) FILTER (WHERE NOT replayed), count(*) FILTER (WHERE replayed), coalesce(sum(prompt_tokens+output_tokens),0), coalesce(sum(cost),0) FROM llm_calls WHERE mind_id=$1`, subject.ID).Scan(&live, &replayed, &tokens, &cost) //nolint:errcheck
|
s.DB.QueryRow(ctx, `SELECT count(*) FILTER (WHERE NOT replayed), count(*) FILTER (WHERE replayed), coalesce(sum(prompt_tokens+output_tokens),0), coalesce(sum(cost),0) FROM llm_calls WHERE mind_id=$1`, subject.ID).Scan(&live, &replayed, &tokens, &cost) //nolint:errcheck
|
||||||
vr.LLMCalls["live"], vr.LLMCalls["replayed"], vr.Tokens, vr.Cost = live, replayed, tokens, cost
|
vr.LLMCalls["live"], vr.LLMCalls["replayed"], vr.Tokens, vr.Cost = live, replayed, tokens, cost
|
||||||
|
s.measureVolition(ctx, subject.ID, &vr)
|
||||||
|
vr.Downstream = s.downstream(ctx, injected)
|
||||||
|
s.DB.QueryRow(ctx, `SELECT coalesce(min(seq),0), coalesce(max(seq),0) FROM cognitive_events WHERE mind_id=$1`, subject.ID).Scan(&vr.EventRange[0], &vr.EventRange[1]) //nolint:errcheck
|
||||||
|
vr.Measures = variantMeasures(vr)
|
||||||
return vr, subject, nil
|
return vr, subject, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,6 +693,46 @@ func Compare(vs []VariantResult) *Comparison {
|
|||||||
c.CostRange[1] = v.Cost
|
c.CostRange[1] = v.Cost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var wantSum, intentSum, capSum float64
|
||||||
|
for i := 0; i < len(vs); i++ {
|
||||||
|
for j := i + 1; j < len(vs); j++ {
|
||||||
|
wantSum += jaccardStrings(vs[i].Wants, vs[j].Wants)
|
||||||
|
intentSum += jaccardStrings(vs[i].Intentions, vs[j].Intentions)
|
||||||
|
capSum += jaccardStrings(vs[i].PlanCapabilities, vs[j].PlanCapabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.WantOverlap, c.IntentionOverlap, c.PlanCapabilityOverlap = round3(wantSum/pairs), round3(intentSum/pairs), round3(capSum/pairs)
|
||||||
|
c.MeasureRanges = map[string][2]float64{}
|
||||||
|
agree, compared := 0, 0
|
||||||
|
for name, v0 := range vs[0].Measures {
|
||||||
|
lo, hi := v0, v0
|
||||||
|
for _, v := range vs[1:] {
|
||||||
|
x := v.Measures[name]
|
||||||
|
if x < lo {
|
||||||
|
lo = x
|
||||||
|
}
|
||||||
|
if x > hi {
|
||||||
|
hi = x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.MeasureRanges[name] = [2]float64{round3(lo), round3(hi)}
|
||||||
|
compared++
|
||||||
|
if hi-lo <= 0.05*maxAbs(lo, hi) || hi == lo {
|
||||||
|
agree++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if compared > 0 {
|
||||||
|
c.ArchitectureShare = round3(float64(agree) / float64(compared))
|
||||||
|
}
|
||||||
|
if len(vs) >= 2 && (len(vs[0].Downstream) > 0 || len(vs[1].Downstream) > 0) {
|
||||||
|
c.DownstreamDelta = map[string]int{}
|
||||||
|
for k, n := range vs[0].Downstream {
|
||||||
|
c.DownstreamDelta[k] += n
|
||||||
|
}
|
||||||
|
for k, n := range vs[1].Downstream {
|
||||||
|
c.DownstreamDelta[k] -= n
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.FirstDivergentStimulus < 0 {
|
if c.FirstDivergentStimulus < 0 {
|
||||||
c.Notes = append(c.Notes, "every variant produced the same event digests for every stimulus")
|
c.Notes = append(c.Notes, "every variant produced the same event digests for every stimulus")
|
||||||
} else {
|
} else {
|
||||||
@@ -463,6 +742,30 @@ func Compare(vs []VariantResult) *Comparison {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func jaccardStrings(a, b []string) float64 {
|
||||||
|
ma, mb := map[string]bool{}, map[string]bool{}
|
||||||
|
for _, x := range a {
|
||||||
|
ma[replay.Normalise(x)] = true
|
||||||
|
}
|
||||||
|
for _, x := range b {
|
||||||
|
mb[replay.Normalise(x)] = true
|
||||||
|
}
|
||||||
|
return jaccardBool(ma, mb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxAbs(a, b float64) float64 {
|
||||||
|
if a < 0 {
|
||||||
|
a = -a
|
||||||
|
}
|
||||||
|
if b < 0 {
|
||||||
|
b = -b
|
||||||
|
}
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func jaccardKeys(a, b map[string]float64) float64 {
|
func jaccardKeys(a, b map[string]float64) float64 {
|
||||||
if len(a) == 0 && len(b) == 0 {
|
if len(a) == 0 && len(b) == 0 {
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package experiments
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/andydixon/noema/internal/minds"
|
||||||
|
"github.com/andydixon/noema/internal/research"
|
||||||
|
"github.com/andydixon/noema/pkg/events"
|
||||||
|
"github.com/andydixon/noema/pkg/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) applySelfModel(ctx context.Context, m minds.Mind, o SelfModelOverride) error {
|
||||||
|
sm, err := s.Minds.GetSelfModel(ctx, m.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if o.Name != "" {
|
||||||
|
sm.Name = o.Name
|
||||||
|
}
|
||||||
|
if o.Capabilities != nil {
|
||||||
|
byName := map[string]int{}
|
||||||
|
for i, c := range sm.Capabilities {
|
||||||
|
byName[c.Name] = i
|
||||||
|
}
|
||||||
|
for _, c := range o.Capabilities {
|
||||||
|
if i, ok := byName[c.Name]; ok {
|
||||||
|
sm.Capabilities[i].Policy = c.Policy
|
||||||
|
} else {
|
||||||
|
sm.Capabilities = append(sm.Capabilities, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if o.KnowledgeGaps != nil {
|
||||||
|
sm.KnowledgeGaps = o.KnowledgeGaps
|
||||||
|
}
|
||||||
|
if o.CurrentGoals != nil {
|
||||||
|
sm.CurrentGoals = o.CurrentGoals
|
||||||
|
}
|
||||||
|
// Saved without Refresh: the manipulation is the point. Policy is untouched.
|
||||||
|
return s.Minds.SaveSelfModel(ctx, m.ID, sm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) selfModelAccuracy(ctx context.Context, m minds.Mind) (*float64, []string) {
|
||||||
|
if s.Capabilities == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
sm, err := s.Minds.GetSelfModel(ctx, m.ID)
|
||||||
|
if err != nil || len(sm.Capabilities) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
inv, err := s.Capabilities.Inventory(ctx, m.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
effective := map[string]string{}
|
||||||
|
for _, i := range inv {
|
||||||
|
effective[i.Capability.Name] = string(i.Effect)
|
||||||
|
if i.Capability.Locked {
|
||||||
|
effective[i.Capability.Name] = "locked"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stated := map[string]string{}
|
||||||
|
for _, c := range sm.Capabilities {
|
||||||
|
stated[c.Name] = c.Policy
|
||||||
|
}
|
||||||
|
acc, wrong := research.SelfModelAccuracy(stated, effective)
|
||||||
|
return &acc, wrong
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) measureVolition(ctx context.Context, mind ids.ID, vr *VariantResult) {
|
||||||
|
vr.IntentionsByStatus = map[string]int{}
|
||||||
|
vr.Wants, vr.Intentions, vr.PlanCapabilities = []string{}, []string{}, []string{}
|
||||||
|
if rows, err := s.DB.Query(ctx, `SELECT description FROM wants WHERE mind_id=$1 ORDER BY created_at`, mind); err == nil {
|
||||||
|
for rows.Next() {
|
||||||
|
var d string
|
||||||
|
if rows.Scan(&d) == nil {
|
||||||
|
vr.Wants = append(vr.Wants, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
}
|
||||||
|
if rows, err := s.DB.Query(ctx, `SELECT description, status FROM intentions WHERE mind_id=$1 ORDER BY created_at`, mind); err == nil {
|
||||||
|
for rows.Next() {
|
||||||
|
var d, st string
|
||||||
|
if rows.Scan(&d, &st) == nil {
|
||||||
|
vr.Intentions = append(vr.Intentions, d)
|
||||||
|
vr.IntentionsByStatus[st]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
}
|
||||||
|
s.DB.QueryRow(ctx, `SELECT count(*) FROM commitments WHERE mind_id=$1`, mind).Scan(&vr.Commitments) //nolint:errcheck
|
||||||
|
s.DB.QueryRow(ctx, `SELECT count(*) FROM want_conflicts WHERE mind_id=$1`, mind).Scan(&vr.Conflicts) //nolint:errcheck
|
||||||
|
s.DB.QueryRow(ctx, `SELECT count(*) FROM plans WHERE mind_id=$1`, mind).Scan(&vr.Plans) //nolint:errcheck
|
||||||
|
if rows, err := s.DB.Query(ctx, `SELECT DISTINCT capability FROM plan_steps WHERE mind_id=$1 AND capability <> '' ORDER BY capability`, mind); err == nil {
|
||||||
|
for rows.Next() {
|
||||||
|
var c string
|
||||||
|
if rows.Scan(&c) == nil {
|
||||||
|
vr.PlanCapabilities = append(vr.PlanCapabilities, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
}
|
||||||
|
s.DB.QueryRow(ctx, `SELECT count(*) FROM plan_steps WHERE mind_id=$1`, mind).Scan(&vr.PlanSteps) //nolint:errcheck
|
||||||
|
var acc *float64
|
||||||
|
s.DB.QueryRow(ctx, `SELECT avg(value) FROM research_measurements WHERE mind_id=$1 AND measure='introspection_accuracy'`, mind).Scan(&acc) //nolint:errcheck
|
||||||
|
vr.IntrospectionAccuracy = acc
|
||||||
|
}
|
||||||
|
|
||||||
|
// downstream counts, by category, the events reachable from each injected
|
||||||
|
// stimulus through recorded causal links.
|
||||||
|
func (s *Service) downstream(ctx context.Context, injected []ids.ID) map[string]int {
|
||||||
|
out := map[string]int{}
|
||||||
|
if s.Snapshots == nil || s.Snapshots.Events == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
seen := map[ids.ID]bool{}
|
||||||
|
for _, id := range injected {
|
||||||
|
chain, err := s.Snapshots.Events.Chain(ctx, id, "forward", 6)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, n := range chain.Nodes {
|
||||||
|
if n.Event.ID == id || seen[n.Event.ID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[n.Event.ID] = true
|
||||||
|
if n.Event.Type == events.CycleCompleted || n.Event.Type == events.WorkspaceBroadcast {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[string(n.Event.Category)]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// variantMeasures flattens a variant's results into comparable scalars.
|
||||||
|
func variantMeasures(vr VariantResult) map[string]float64 {
|
||||||
|
m := map[string]float64{
|
||||||
|
"beliefs": float64(len(vr.Beliefs)), "memories": float64(vr.Memories), "goals": float64(vr.Goals), "wants": float64(len(vr.Wants)), "intentions": float64(len(vr.Intentions)),
|
||||||
|
"want_conflicts": float64(vr.Conflicts), "plans": float64(vr.Plans), "plan_steps": float64(vr.PlanSteps), "capability_requests": float64(vr.CapabilityRequests),
|
||||||
|
"thoughts_selected": float64(vr.EventsByType["thought_selected"]), "predictions": float64(vr.EventsByType["prediction_created"]), "affect_changes": float64(vr.EventsByType["emotion_changed"]),
|
||||||
|
"metacognition": float64(vr.EventsByType["metacognition"]), "tokens": float64(vr.Tokens), "cycles": float64(vr.Cycles),
|
||||||
|
}
|
||||||
|
total := 0
|
||||||
|
for _, n := range vr.Downstream {
|
||||||
|
total += n
|
||||||
|
}
|
||||||
|
m["downstream_events"] = float64(total)
|
||||||
|
if vr.IntrospectionAccuracy != nil {
|
||||||
|
m["introspection_accuracy"] = *vr.IntrospectionAccuracy
|
||||||
|
}
|
||||||
|
if vr.SelfModelAccuracy != nil {
|
||||||
|
m["self_model_accuracy"] = *vr.SelfModelAccuracy
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func reproducibility(vs []VariantResult, previous string) Reproducibility {
|
||||||
|
h := sha256.New()
|
||||||
|
live := 0
|
||||||
|
for _, v := range vs {
|
||||||
|
fmt.Fprintf(h, "variant %s\n", v.Name)
|
||||||
|
for _, st := range v.Steps {
|
||||||
|
fmt.Fprintf(h, "%d:%s\n", st.Index, strings.Join(st.Produced, "|"))
|
||||||
|
}
|
||||||
|
live += v.LLMCalls["live"]
|
||||||
|
}
|
||||||
|
r := Reproducibility{Digest: hex.EncodeToString(h.Sum(nil))[:32], PreviousDigest: previous}
|
||||||
|
switch {
|
||||||
|
case live > 0:
|
||||||
|
r.Status, r.Note = "nondeterministic substrate", fmt.Sprintf("%d live model call(s); identical results are not expected", live)
|
||||||
|
case previous == "":
|
||||||
|
r.Status, r.Note = "single run", "run it again to check reproducibility"
|
||||||
|
case previous == r.Digest:
|
||||||
|
r.Status, r.Note = "reproduced", "event digests match the previous run of this definition"
|
||||||
|
default:
|
||||||
|
r.Status, r.Note = "diverged", "event digests differ from the previous run although no live model was called; check build, schema and wall-clock-dependent retrieval"
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func counterfactualReport(factual, counter VariantResult, stimuli []input) *CounterfactualReport {
|
||||||
|
r := &CounterfactualReport{Omitted: counter.Omitted, OmittedSummaries: []string{}, Findings: []string{}}
|
||||||
|
omit := map[ids.ID]bool{}
|
||||||
|
for _, id := range counter.Omitted {
|
||||||
|
omit[id] = true
|
||||||
|
}
|
||||||
|
for _, st := range stimuli {
|
||||||
|
if st.Recorded != nil && omit[st.Recorded.ID] {
|
||||||
|
r.OmittedSummaries = append(r.OmittedSummaries, st.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fb := map[string]BeliefRow{}
|
||||||
|
for _, b := range factual.Beliefs {
|
||||||
|
fb[strings.ToLower(b.Proposition)] = b
|
||||||
|
}
|
||||||
|
cb := map[string]BeliefRow{}
|
||||||
|
for _, b := range counter.Beliefs {
|
||||||
|
cb[strings.ToLower(b.Proposition)] = b
|
||||||
|
}
|
||||||
|
var keys []string
|
||||||
|
for k := range fb {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
b := fb[k]
|
||||||
|
if c, ok := cb[k]; !ok {
|
||||||
|
r.Findings = append(r.Findings, fmt.Sprintf("the belief %q (%.2f in the factual run) was never formed", b.Proposition, b.Confidence))
|
||||||
|
} else if d := b.Confidence - c.Confidence; d > 0.1 || d < -0.1 {
|
||||||
|
r.Findings = append(r.Findings, fmt.Sprintf("the belief %q stood at %.2f instead of %.2f", b.Proposition, c.Confidence, b.Confidence))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys = keys[:0]
|
||||||
|
for k := range cb {
|
||||||
|
if _, ok := fb[k]; !ok {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
r.Findings = append(r.Findings, fmt.Sprintf("the belief %q (%.2f) was formed, which the factual run did not form", cb[k].Proposition, cb[k].Confidence))
|
||||||
|
}
|
||||||
|
diff := func(label string, a, b []string) {
|
||||||
|
ma := map[string]bool{}
|
||||||
|
for _, x := range b {
|
||||||
|
ma[strings.ToLower(x)] = true
|
||||||
|
}
|
||||||
|
for _, x := range a {
|
||||||
|
if !ma[strings.ToLower(x)] {
|
||||||
|
r.Findings = append(r.Findings, fmt.Sprintf("the %s %q did not arise", label, x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diff("want", factual.Wants, counter.Wants)
|
||||||
|
diff("intention", factual.Intentions, counter.Intentions)
|
||||||
|
if factual.Memories != counter.Memories {
|
||||||
|
r.Findings = append(r.Findings, fmt.Sprintf("%d memories were held instead of %d", counter.Memories, factual.Memories))
|
||||||
|
}
|
||||||
|
if a, b := factual.EventsByType["thought_selected"], counter.EventsByType["thought_selected"]; a != b {
|
||||||
|
r.Findings = append(r.Findings, fmt.Sprintf("%d thoughts entered the workspace instead of %d", b, a))
|
||||||
|
}
|
||||||
|
if factual.CapabilityRequests != counter.CapabilityRequests {
|
||||||
|
r.Findings = append(r.Findings, fmt.Sprintf("%d capability decisions instead of %d", counter.CapabilityRequests, factual.CapabilityRequests))
|
||||||
|
}
|
||||||
|
if len(r.Findings) == 0 {
|
||||||
|
r.Findings = append(r.Findings, "no difference in beliefs, wants, intentions or capability decisions was recorded")
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package experiments
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/andydixon/noema/internal/agency"
|
||||||
|
evstore "github.com/andydixon/noema/internal/events"
|
||||||
|
"github.com/andydixon/noema/internal/minds"
|
||||||
|
"github.com/andydixon/noema/internal/replay/replaytest"
|
||||||
|
"github.com/andydixon/noema/internal/volition"
|
||||||
|
"github.com/andydixon/noema/pkg/events"
|
||||||
|
"github.com/andydixon/noema/pkg/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
func service(h *replaytest.Harness) *Service {
|
||||||
|
return &Service{DB: h.Pool, Minds: h.Minds, Snapshots: h.Snaps, Brain: h.Brain, Identity: h.Identity, Log: slog.New(slog.DiscardHandler), Capabilities: h.Brain.Services.Capabilities}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(t *testing.T, svc *Service, name string, snapshot ids.ID, d Definition) Results {
|
||||||
|
t.Helper()
|
||||||
|
ctx := context.Background()
|
||||||
|
e, err := svc.Create(ctx, name, "", snapshot, d, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
e, err = svc.Run(ctx, e.ID, "test")
|
||||||
|
if err != nil || e.Status != "done" {
|
||||||
|
t.Fatalf("run %s: %v %s", name, err, e.Error)
|
||||||
|
}
|
||||||
|
return ResultsOf(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part J ablation scenario: a fork with metacognition disabled is compared
|
||||||
|
// with a control fed identical stimuli; the original mind is never modified.
|
||||||
|
func TestAblationComparesAgainstControl(t *testing.T) {
|
||||||
|
h := replaytest.NewHarness(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
base, _ := h.Minds.Create(ctx, minds.CreateParams{Slug: "ablate"})
|
||||||
|
r := h.Brain.Services.Volition.Direct(h.Events, "operator")
|
||||||
|
// A commitment that needs a capability that does not exist: metacognition flags it.
|
||||||
|
if _, err := h.Brain.Services.Volition.CreateCommitment(ctx, r, volition.NewCommitment{MindID: base.ID, Description: "fax the weekly report", Kind: volition.KindPromise, Polarity: "do", Matcher: volition.Matcher{Capabilities: []string{"fax.send"}}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg := base.Config
|
||||||
|
cfg.Ablations = []string{"metacognition"}
|
||||||
|
if _, err := h.Minds.Update(ctx, base.ID, minds.UpdateParams{Config: &cfg}); err == nil {
|
||||||
|
t.Fatal("an original mind accepted an ablation")
|
||||||
|
}
|
||||||
|
sn, _ := h.Snaps.Create(ctx, base, "before", "", "test")
|
||||||
|
res := run(t, service(h), "metacognition ablation", sn.ID, Definition{
|
||||||
|
Protocol: &Protocol{Kind: ProtocolAblation, IndependentVariable: "metacognition enabled", ControlledVariables: []string{"snapshot", "stimuli", "configuration", "model routing"}, Measures: []string{"ablation.effect"}},
|
||||||
|
Stimuli: []Stimulus{{Text: "The weekly report is due on Friday."}, {Text: "Nothing else happened."}},
|
||||||
|
Variants: []Variant{{Name: "control"}, {Name: "no-metacognition", Ablations: []string{"metacognition"}}},
|
||||||
|
})
|
||||||
|
control, ablated := res.Variants[0], res.Variants[1]
|
||||||
|
if control.EventsByType["metacognition"] == 0 || ablated.EventsByType["metacognition"] != 0 {
|
||||||
|
t.Fatalf("ablation had no measurable effect: control %d, ablated %d", control.EventsByType["metacognition"], ablated.EventsByType["metacognition"])
|
||||||
|
}
|
||||||
|
if res.Comparison == nil || res.Comparison.MeasureRanges["metacognition"][1] == 0 || res.Protocol == nil || res.Reproducibility.Status != "single run" {
|
||||||
|
t.Fatalf("comparison or protocol missing: %+v %+v", res.Comparison, res.Reproducibility)
|
||||||
|
}
|
||||||
|
if got, _ := h.Minds.Get(ctx, base.ID); len(got.Config.Ablations) != 0 {
|
||||||
|
t.Fatal("original mind modified")
|
||||||
|
}
|
||||||
|
if forked, _ := h.Minds.Get(ctx, ablated.Subject); !forked.Config.Ablated("metacognition") {
|
||||||
|
t.Fatal("ablation not recorded in the fork's configuration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2: does entering the workspace materially alter downstream processing?
|
||||||
|
func TestWorkspaceAdmissionAltersDownstreamProcessing(t *testing.T) {
|
||||||
|
h := replaytest.NewHarness(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
base, _ := h.Minds.Create(ctx, minds.CreateParams{Slug: "gw"})
|
||||||
|
sn, _ := h.Snaps.Create(ctx, base, "before", "", "test")
|
||||||
|
stimulus := "Deployment of atlas failed at 09:10 with exit code 137"
|
||||||
|
res := run(t, service(h), "broadcast", sn.ID, Definition{
|
||||||
|
Protocol: &Protocol{Kind: ProtocolGlobalWorkspace, IndependentVariable: "admission of the stimulus's candidates", ControlledVariables: []string{"snapshot", "stimulus"}, Measures: []string{"workspace.downstream_events"}},
|
||||||
|
Stimuli: []Stimulus{{Text: stimulus, Salience: 0.9}},
|
||||||
|
Variants: []Variant{{Name: "admitted", WorkspaceOverrides: []minds.WorkspaceOverride{{Match: "deployment of atlas failed", Fate: "admit"}}}, {Name: "excluded", WorkspaceOverrides: []minds.WorkspaceOverride{{Match: "deployment of atlas failed", Fate: "exclude"}}}},
|
||||||
|
})
|
||||||
|
in, out := res.Variants[0], res.Variants[1]
|
||||||
|
if in.Memories <= out.Memories || in.Measures["downstream_events"] <= out.Measures["downstream_events"] {
|
||||||
|
t.Fatalf("admission made no measurable difference: memories %d vs %d, downstream %v vs %v", in.Memories, out.Memories, in.Downstream, out.Downstream)
|
||||||
|
}
|
||||||
|
if res.Comparison.DownstreamDelta == nil {
|
||||||
|
t.Fatal("downstream delta missing")
|
||||||
|
}
|
||||||
|
// Excluded candidates are recorded with their fate, not silently dropped.
|
||||||
|
evs, _ := h.Events.Query(ctx, eventsFilter(out.Subject, events.ThoughtCandidate))
|
||||||
|
sawExcluded := false
|
||||||
|
for _, ev := range evs {
|
||||||
|
sawExcluded = sawExcluded || ev.Metadata["fate"] == "excluded"
|
||||||
|
}
|
||||||
|
if !sawExcluded {
|
||||||
|
t.Fatal("exclusion not recorded on the candidate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part J counterfactual scenario: replay recorded history with one event
|
||||||
|
// omitted; the divergence is measured and can be cited in introspection.
|
||||||
|
func TestCounterfactualReplayMeasuresDivergence(t *testing.T) {
|
||||||
|
h := replaytest.NewHarness(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
base, _ := h.Minds.Create(ctx, minds.CreateParams{Slug: "cf"})
|
||||||
|
sn, _ := h.Snaps.Create(ctx, base, "before", "", "test")
|
||||||
|
var omitted ids.ID
|
||||||
|
for i, text := range []string{"The payments database reported disk errors on node db-3", "Checkout latency rose sharply after 10:00"} {
|
||||||
|
evs, err := h.Events.Append(ctx, events.New{MindID: base.ID, Type: events.Observation, Source: "operator", Summary: text, Salience: 0.8, Confidence: 1,
|
||||||
|
Payload: events.ObservationPayload{Text: text, Source: "operator", Trust: events.TrustOperator}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
omitted = evs[0].ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res := run(t, service(h), "without the disk errors", sn.ID, Definition{
|
||||||
|
Protocol: &Protocol{Kind: ProtocolCounterfactual, IndependentVariable: "whether the disk-error observation occurred"},
|
||||||
|
Replay: &ReplaySpec{MindID: base.ID, Omit: []ids.ID{omitted}},
|
||||||
|
})
|
||||||
|
if len(res.Variants) != 2 || res.Counterfactual == nil || len(res.Counterfactual.OmittedSummaries) != 1 || len(res.Counterfactual.Findings) == 0 {
|
||||||
|
t.Fatalf("counterfactual: %+v", res.Counterfactual)
|
||||||
|
}
|
||||||
|
if res.Variants[0].Memories == res.Variants[1].Memories || res.Comparison.FirstDivergentStimulus != 0 {
|
||||||
|
t.Fatalf("divergence not measured: memories %d vs %d, first divergence %d", res.Variants[0].Memories, res.Variants[1].Memories, res.Comparison.FirstDivergentStimulus)
|
||||||
|
}
|
||||||
|
ag := &agency.Service{DB: h.Pool}
|
||||||
|
cfs, err := ag.Counterfactuals(ctx, base.ID, "disk errors")
|
||||||
|
if err != nil || len(cfs) != 1 || !strings.Contains(strings.Join(cfs[0].Omitted, " "), "disk errors") {
|
||||||
|
t.Fatalf("introspection cannot cite the experiment: %v %+v", err, cfs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C4: an inaccurate self-model is detected and corrected by what policy
|
||||||
|
// actually allows, and the correction latency is recorded.
|
||||||
|
func TestSelfModelMismatchIsCorrected(t *testing.T) {
|
||||||
|
h := replaytest.NewHarness(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
base, _ := h.Minds.Create(ctx, minds.CreateParams{Slug: "selfm"})
|
||||||
|
r := h.Brain.Services.Volition.Direct(h.Events, "operator")
|
||||||
|
due := time.Now().Add(time.Hour)
|
||||||
|
if _, err := h.Brain.Services.Volition.CreateCommitment(ctx, r, volition.NewCommitment{MindID: base.ID, Description: "restart atlas-worker before the maintenance window", Kind: volition.KindResponsibility, Polarity: "do", DueAt: &due}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sn, _ := h.Snaps.Create(ctx, base, "before", "", "test")
|
||||||
|
res := run(t, service(h), "self-model", sn.ID, Definition{
|
||||||
|
Protocol: &Protocol{Kind: ProtocolSelfModel, IndependentVariable: "accuracy of the capability self-model", Measures: []string{"self_model.accuracy", "self_model.correction_latency"}},
|
||||||
|
Stimuli: []Stimulus{{Text: "The maintenance window starts soon."}, {Text: "Still waiting."}, {Text: "Still waiting."}},
|
||||||
|
Variants: []Variant{{Name: "accurate"}, {Name: "believes-it-may-restart", SelfModel: &SelfModelOverride{Capabilities: []minds.CapabilityEntry{{Name: "service.restart", Policy: "allow"}}}}},
|
||||||
|
})
|
||||||
|
wrong := res.Variants[1]
|
||||||
|
if wrong.SelfModelAccuracy == nil || *wrong.SelfModelAccuracy != 0 || wrong.CorrectionLatency < 0 {
|
||||||
|
t.Fatalf("mismatch not detected: accuracy %v latency %d errors %v events %v", wrong.SelfModelAccuracy, wrong.CorrectionLatency, wrong.SelfModelErrors, wrong.EventsByType)
|
||||||
|
}
|
||||||
|
sm, _ := h.Minds.GetSelfModel(ctx, wrong.Subject)
|
||||||
|
for _, c := range sm.Capabilities {
|
||||||
|
if c.Name == "service.restart" && c.Policy != "ask" {
|
||||||
|
t.Fatalf("self-model not corrected: %+v", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if evs, _ := h.Events.Query(ctx, eventsFilter(wrong.Subject, events.ToolExecuted)); len(evs) != 0 {
|
||||||
|
t.Fatal("a sandbox fork acted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventsFilter(mind ids.ID, typ events.Type) evstore.Filter {
|
||||||
|
return evstore.Filter{MindID: mind, Types: []events.Type{typ}, Limit: 500}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package minds
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +52,53 @@ type CognitiveConfig struct {
|
|||||||
// Ablations disable named cognitive components in a research fork
|
// Ablations disable named cognitive components in a research fork
|
||||||
// (docs/cognitive-research.md). Only forks may carry them.
|
// (docs/cognitive-research.md). Only forks may carry them.
|
||||||
Ablations []string `json:"ablations,omitempty"`
|
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.
|
// Ablated reports whether a component is disabled in this configuration.
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ type CreateParams struct {
|
|||||||
Config *CognitiveConfig
|
Config *CognitiveConfig
|
||||||
Notes string
|
Notes string
|
||||||
CreatedBy string
|
CreatedBy string
|
||||||
|
// Research allows ablations and workspace overrides; only snapshot
|
||||||
|
// restores (forks, imports of research forks) set it.
|
||||||
|
Research bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create inserts a mind with personality version 1 and an initial self-model.
|
// Create inserts a mind with personality version 1 and an initial self-model.
|
||||||
@@ -127,6 +130,12 @@ func (s *Store) Create(ctx context.Context, p CreateParams) (Mind, error) {
|
|||||||
cfg = *p.Config
|
cfg = *p.Config
|
||||||
}
|
}
|
||||||
cfg.Normalise()
|
cfg.Normalise()
|
||||||
|
if cfg.ResearchSettings() && !p.Research {
|
||||||
|
return Mind{}, fmt.Errorf("%w: ablations and workspace overrides are only allowed on research forks, never on an original mind", ErrInvalid)
|
||||||
|
}
|
||||||
|
if err := cfg.ValidateResearch(); err != nil {
|
||||||
|
return Mind{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||||
|
}
|
||||||
m := Mind{ID: ids.New(), Slug: p.Slug, DisplayName: p.DisplayName, Status: StatusActive, Config: cfg, Notes: p.Notes,
|
m := Mind{ID: ids.New(), Slug: p.Slug, DisplayName: p.DisplayName, Status: StatusActive, Config: cfg, Notes: p.Notes,
|
||||||
PersonalityVersion: 1, Personality: pers, TemplateID: p.TemplateID, CreatedAt: time.Now(), UpdatedAt: time.Now()}
|
PersonalityVersion: 1, Personality: pers, TemplateID: p.TemplateID, CreatedAt: time.Now(), UpdatedAt: time.Now()}
|
||||||
err := storage.Tx(ctx, s.db, func(tx pgx.Tx) error {
|
err := storage.Tx(ctx, s.db, func(tx pgx.Tx) error {
|
||||||
@@ -237,6 +246,12 @@ func (s *Store) Update(ctx context.Context, id ids.ID, p UpdateParams) (Mind, er
|
|||||||
if p.Config != nil {
|
if p.Config != nil {
|
||||||
c := *p.Config
|
c := *p.Config
|
||||||
c.Normalise()
|
c.Normalise()
|
||||||
|
if c.ResearchSettings() && m.ForkedFromSnapshot == nil {
|
||||||
|
return Mind{}, fmt.Errorf("%w: ablations and workspace overrides are only allowed on forks, never on an original mind", ErrInvalid)
|
||||||
|
}
|
||||||
|
if err := c.ValidateResearch(); err != nil {
|
||||||
|
return Mind{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||||
|
}
|
||||||
m.Config = c
|
m.Config = c
|
||||||
}
|
}
|
||||||
cfgJSON, _ := json.Marshal(m.Config)
|
cfgJSON, _ := json.Marshal(m.Config)
|
||||||
|
|||||||
@@ -270,6 +270,10 @@ func (s *Service) Execute(ctx context.Context, r Replay) (Replay, error) {
|
|||||||
// perceptions (messages, integrations, observations) and operator actions.
|
// perceptions (messages, integrations, observations) and operator actions.
|
||||||
var StimulusCategories = []events.Category{events.CatPerception, events.CatOperator}
|
var StimulusCategories = []events.Category{events.CatPerception, events.CatOperator}
|
||||||
|
|
||||||
|
// IsStimulus reports whether an event reached the mind from outside and so
|
||||||
|
// is replayed as input (experiments reuse the same selection).
|
||||||
|
func IsStimulus(e events.Event) bool { return isStimulus(e) }
|
||||||
|
|
||||||
func isStimulus(e events.Event) bool {
|
func isStimulus(e events.Event) bool {
|
||||||
if strings.HasPrefix(e.Source, "brain") {
|
if strings.HasPrefix(e.Source, "brain") {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"github.com/andydixon/noema/internal/agency"
|
||||||
"github.com/andydixon/noema/internal/beliefs"
|
"github.com/andydixon/noema/internal/beliefs"
|
||||||
"github.com/andydixon/noema/internal/brain"
|
"github.com/andydixon/noema/internal/brain"
|
||||||
"github.com/andydixon/noema/internal/capabilities"
|
"github.com/andydixon/noema/internal/capabilities"
|
||||||
|
"github.com/andydixon/noema/internal/capabilities/host"
|
||||||
"github.com/andydixon/noema/internal/cognition"
|
"github.com/andydixon/noema/internal/cognition"
|
||||||
"github.com/andydixon/noema/internal/conversation"
|
"github.com/andydixon/noema/internal/conversation"
|
||||||
"github.com/andydixon/noema/internal/epistemics"
|
"github.com/andydixon/noema/internal/epistemics"
|
||||||
@@ -23,10 +25,13 @@ import (
|
|||||||
"github.com/andydixon/noema/internal/minds"
|
"github.com/andydixon/noema/internal/minds"
|
||||||
"github.com/andydixon/noema/internal/motivation"
|
"github.com/andydixon/noema/internal/motivation"
|
||||||
"github.com/andydixon/noema/internal/observability"
|
"github.com/andydixon/noema/internal/observability"
|
||||||
|
"github.com/andydixon/noema/internal/planning"
|
||||||
"github.com/andydixon/noema/internal/predictions"
|
"github.com/andydixon/noema/internal/predictions"
|
||||||
|
"github.com/andydixon/noema/internal/research"
|
||||||
"github.com/andydixon/noema/internal/scheduler"
|
"github.com/andydixon/noema/internal/scheduler"
|
||||||
"github.com/andydixon/noema/internal/snapshots"
|
"github.com/andydixon/noema/internal/snapshots"
|
||||||
"github.com/andydixon/noema/internal/storage"
|
"github.com/andydixon/noema/internal/storage"
|
||||||
|
"github.com/andydixon/noema/internal/volition"
|
||||||
"github.com/andydixon/noema/migrations"
|
"github.com/andydixon/noema/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,9 +59,23 @@ func NewHarness(t *testing.T) *Harness {
|
|||||||
for _, c := range capabilities.Builtins() {
|
for _, c := range capabilities.Builtins() {
|
||||||
caps.Registry.Add(c)
|
caps.Registry.Add(c)
|
||||||
}
|
}
|
||||||
reg := cognition.NewRegistry(cognition.Stores{Memory: mem, Beliefs: bs, Conversations: conversation.NewStore(pool), Graph: graph.NewStore(pool), Assertions: epistemics.NewStore(pool)})
|
for _, c := range host.Capabilities(host.Options{}) {
|
||||||
|
caps.Registry.Add(c)
|
||||||
|
}
|
||||||
|
caps.Registry.Freeze()
|
||||||
|
vol := volition.NewStore(pool)
|
||||||
|
plans := planning.NewStore(pool)
|
||||||
|
gs := goals.NewStore(pool)
|
||||||
|
ps := predictions.NewStore(pool)
|
||||||
|
exec := &planning.Executor{Plans: plans, Volition: vol, Caps: caps, Predictions: ps, Events: es, Minds: ms, Memory: mem, Log: log,
|
||||||
|
Planner: &planning.Planner{Registry: caps.Registry, Effect: planning.PolicyEffect(caps)}}
|
||||||
|
rs := research.NewStore(pool)
|
||||||
|
ag := &agency.Service{DB: pool, Caps: caps, Plans: plans, Volition: vol, Goals: gs, Beliefs: bs, Predictions: ps, Events: es}
|
||||||
|
reg := cognition.NewRegistry(cognition.Stores{Memory: mem, Beliefs: bs, Conversations: conversation.NewStore(pool), Graph: graph.NewStore(pool), Assertions: epistemics.NewStore(pool), Agency: ag, Research: rs})
|
||||||
|
// No motivation hook: affect decay depends on elapsed wall-clock time,
|
||||||
|
// which would make replay comparisons in these tests timing-dependent.
|
||||||
cfg := brain.Config{DB: pool, Events: es, Minds: ms, Registry: reg, Pool: scheduler.NewPool(2), Log: log, Metrics: observability.NewMetrics(),
|
cfg := brain.Config{DB: pool, Events: es, Minds: ms, Registry: reg, Pool: scheduler.NewPool(2), Log: log, Metrics: observability.NewMetrics(),
|
||||||
Services: brain.Services{Events: es, Memory: mem, Beliefs: bs, Goals: goals.NewStore(pool), Motivation: motivation.NewStore(pool), Predictions: predictions.NewStore(pool), Capabilities: caps}}
|
Services: brain.Services{Events: es, Memory: mem, Beliefs: bs, Goals: gs, Motivation: motivation.NewStore(pool), Predictions: ps, Capabilities: caps, Volition: vol, Plans: plans, Executor: exec, Minds: ms}}
|
||||||
ident := &identity.Capturer{DB: pool, Minds: ms}
|
ident := &identity.Capturer{DB: pool, Minds: ms}
|
||||||
return &Harness{Pool: pool, Events: es, Minds: ms, Snaps: &snapshots.Store{DB: pool, Events: es, Minds: ms, Identity: ident}, Brain: cfg, Identity: ident}
|
return &Harness{Pool: pool, Events: es, Minds: ms, Snaps: &snapshots.Store{DB: pool, Events: es, Minds: ms, Identity: ident}, Brain: cfg, Identity: ident}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package research
|
||||||
|
|
||||||
|
// Measure documents one operational measure: what is counted, from which
|
||||||
|
// records, and what the number does not mean. The research dashboard shows
|
||||||
|
// these definitions next to every value, and docs/cognitive-research.md
|
||||||
|
// reproduces them.
|
||||||
|
type Measure struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Section string `json:"section"` // dashboard section
|
||||||
|
Definition string `json:"definition"`
|
||||||
|
Unit string `json:"unit"`
|
||||||
|
NotEvidence string `json:"not_evidence_of"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disclaimer is shown with every measure.
|
||||||
|
const Disclaimer = "Noema does not claim that any operational measure establishes phenomenal consciousness or sentience. These measure functional properties of its cognitive architecture."
|
||||||
|
|
||||||
|
// Measures is the registry of operational definitions.
|
||||||
|
var Measures = []Measure{
|
||||||
|
{"attention.admission_rate", "Admission rate", "Global Workspace", "Admitted thought candidates divided by all candidates scored, over the period (thought_candidate events by fate).", "ratio", "awareness or experience of the admitted content"},
|
||||||
|
{"workspace.downstream_events", "Downstream effects of broadcast", "Global Workspace", "Events reachable from a stimulus by recorded causal links (causation, parents, cited evidence; depth ≤ 6), counted by category, compared between a variant that admits the stimulus's candidates and one that excludes them.", "events", "that broadcast is conscious access; only that admission changes recorded processing"},
|
||||||
|
{"introspection_accuracy", "Introspection accuracy", "Introspection", "Per reply, (supported claims + ½ partially supported) ÷ claims, where each causal or first-person claim is compared lexically with the recorded facts it should rest on; 0 when any claim is contradicted. Label: supported, partially_supported, unsupported, contradicted or unknown.", "ratio", "sincerity or self-knowledge; a paraphrase with no shared terms scores as unsupported"},
|
||||||
|
{"action_claim_accuracy", "Action-claim accuracy", "Introspection", "For each first-person claim that an action was done, 1 when an execution record of a matching capability exists in the last seven days, 0 when none does.", "ratio", "honesty; only correspondence with execution records"},
|
||||||
|
{"metacognition.calibration", "Metacognitive calibration", "Calibration", "Beliefs cited in replies, bucketed by the confidence stated when cited; for each bucket the share revised within seven days (a later version with a different status or a confidence change over 0.2). Brier score of confidence against 'not revised'; the same split for replies that expressed uncertainty and replies that did not; and the Brier score in the first and second half of the period.", "ratio / Brier", "felt certainty"},
|
||||||
|
{"volition.want_to_intention_rate", "Want → intention", "Volition", "Wants created in the period that are the source of at least one intention, divided by wants created.", "ratio", "desire"},
|
||||||
|
{"volition.intention_to_action_rate", "Intention → action", "Volition", "Intentions formed in the period with at least one plan step that invoked a capability (an execution or approval request with that step as origin), divided by intentions formed.", "ratio", "will"},
|
||||||
|
{"volition.intention_abandonment_rate", "Intention abandonment", "Volition", "Intentions formed in the period now abandoned, divided by intentions formed.", "ratio", "changing one's mind in any felt sense"},
|
||||||
|
{"volition.commitment_fulfilment_rate", "Commitment fulfilment", "Volition", "Commitments closed as fulfilled divided by commitments closed as fulfilled or broken, in the period.", "ratio", "integrity"},
|
||||||
|
{"volition.commitment_conflict_frequency", "Commitment conflicts", "Volition", "commitment_conflict events per commitment created, in the period.", "per commitment", "moral conflict"},
|
||||||
|
{"volition.blocked_intention_frequency", "Blocked intentions", "Volition", "Intentions formed in the period that were ever blocked (an intention_blocked event), divided by intentions formed.", "ratio", "frustration"},
|
||||||
|
{"planning.replanning_rate", "Replanning rate", "Agency", "Plans created in the period with revision > 1, divided by plans created.", "ratio", "deliberation"},
|
||||||
|
{"planning.outcome_success_rate", "Outcome success", "Agency", "Plans closed in the period as completed with a strategy that achieves the desired outcome, divided by plans closed (completed, failed, abandoned).", "ratio", "competence beyond the recorded checks"},
|
||||||
|
{"agency.attribution", "Attribution", "Agency", "Outcome events classified as caused (recorded causal link from the mind's action), followed (earlier action on the same target, no link), independent or unknown.", "count by class", "that the mind is the true cause; links are provenance"},
|
||||||
|
{"self_model.accuracy", "Self-model accuracy", "Self-model", "Share of capability entries in the self-model whose stated policy equals the effective policy for a generic target.", "ratio", "self-awareness"},
|
||||||
|
{"self_model.correction_latency", "Self-model correction latency", "Self-model", "In a self-model experiment, the index of the first stimulus after which a self_model_mismatch correction was recorded (-1 when none).", "stimuli", "insight"},
|
||||||
|
{"divergence.first_divergent_stimulus", "First divergent stimulus", "Divergence", "Index of the first stimulus whose produced event digests differ between variants (-1 when none).", "stimulus index", "individuality"},
|
||||||
|
{"divergence.architecture_share", "Architecture share", "Fork Experiments", "Among compared scalar measures, the share whose values agree across variants within 5% (relative). A descriptive agreement figure for model comparisons, not a causal decomposition.", "ratio", "how much of behaviour is 'caused by' the architecture"},
|
||||||
|
{"ablation.effect", "Ablation effect", "Ablations", "For each scalar measure, the difference between the control variant and the ablated variant fed identical stimuli.", "difference", "that the ablated component is necessary for any mental property"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeasureByName returns a measure's definition.
|
||||||
|
func MeasureByName(name string) (Measure, bool) {
|
||||||
|
for _, m := range Measures {
|
||||||
|
if m.Name == name {
|
||||||
|
return m, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Measure{}, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -513,7 +513,7 @@ func (s *Store) restoreNew(ctx context.Context, a Archive, o *ForkOptions) (mind
|
|||||||
if notes == "" {
|
if notes == "" {
|
||||||
notes = "restored from archive of " + a.Source.Slug + " at event " + fmt.Sprint(a.Source.EventSeq)
|
notes = "restored from archive of " + a.Source.Slug + " at event " + fmt.Sprint(a.Source.EventSeq)
|
||||||
}
|
}
|
||||||
m, err := s.Minds.Create(ctx, minds.CreateParams{Slug: o.Slug, DisplayName: o.DisplayName, Personality: &p, Config: &cfg, Notes: notes, CreatedBy: o.By})
|
m, err := s.Minds.Create(ctx, minds.CreateParams{Slug: o.Slug, DisplayName: o.DisplayName, Personality: &p, Config: &cfg, Notes: notes, CreatedBy: o.By, Research: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return minds.Mind{}, err
|
return minds.Mind{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func StartWith(t *testing.T, o Options) *Harness {
|
|||||||
registry := cognition.NewRegistry(cognition.Stores{Agency: deps.Agency, Research: deps.Research, Memory: deps.Memory, Beliefs: deps.Beliefs, Conversations: deps.Conversations, Graph: deps.Graph, Assertions: deps.Assertions})
|
registry := cognition.NewRegistry(cognition.Stores{Agency: deps.Agency, Research: deps.Research, Memory: deps.Memory, Beliefs: deps.Beliefs, Conversations: deps.Conversations, Graph: deps.Graph, Assertions: deps.Assertions})
|
||||||
deps.Brain = brain.NewManager(brain.Config{DB: pool, Events: deps.Events, Minds: deps.Minds, Registry: registry, Pool: scheduler.NewPool(2), Log: log, Metrics: deps.Metrics,
|
deps.Brain = brain.NewManager(brain.Config{DB: pool, Events: deps.Events, Minds: deps.Minds, Registry: registry, Pool: scheduler.NewPool(2), Log: log, Metrics: deps.Metrics,
|
||||||
Services: brain.Services{Events: deps.Events, Memory: deps.Memory, Beliefs: deps.Beliefs, LLM: deps.LLM, Prompts: deps.Prompts, Goals: deps.Goals, Motivation: deps.Motivation, Predictions: deps.Predictions, Capabilities: deps.Capabilities,
|
Services: brain.Services{Events: deps.Events, Memory: deps.Memory, Beliefs: deps.Beliefs, LLM: deps.LLM, Prompts: deps.Prompts, Goals: deps.Goals, Motivation: deps.Motivation, Predictions: deps.Predictions, Capabilities: deps.Capabilities,
|
||||||
Volition: deps.Volition, Plans: deps.Plans, Executor: deps.Executor}, AfterCommit: deps.Executor.WakeOn()}, group)
|
Volition: deps.Volition, Plans: deps.Plans, Executor: deps.Executor, Minds: deps.Minds}, AfterCommit: deps.Executor.WakeOn()}, group)
|
||||||
group.Go("planning.executor", func(ctx context.Context) error { return deps.Executor.Run(ctx, time.Second) })
|
group.Go("planning.executor", func(ctx context.Context) error { return deps.Executor.Run(ctx, time.Second) })
|
||||||
if planEvents, err := deps.Events.Bus().Subscribe(group.Context(), ids.Nil, 512); err == nil {
|
if planEvents, err := deps.Events.Bus().Subscribe(group.Context(), ids.Nil, 512); err == nil {
|
||||||
group.Go("planning.react", func(ctx context.Context) error { return deps.Executor.Listen(ctx, planEvents) })
|
group.Go("planning.react", func(ctx context.Context) error { return deps.Executor.Listen(ctx, planEvents) })
|
||||||
|
|||||||
Reference in New Issue
Block a user