From 2ecdbc39b5dd6af8bcd30d61457f9bd0d0661cb7 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Thu, 24 Sep 2026 21:16:48 +0100 Subject: [PATCH] feat(ui): wants, intentions, commitments, plans, agency timeline, why, research - Wants: active wants by effective strength with origin, means, drives, values, goal, reinforcement, suppressions and each conflict with its resolution rule; closed wants with their resolution. - Intentions grouped by status with blocked reasons and plans, plus the wants that did not become intentions and why. An intention page shows sources, subordinate intentions, formation terms, every plan revision as a dependency graph, decisions and the correlated timeline; operators can abandon it. - Commitments with polarity, exception, matcher, weight and due time, live conflicts between commitments, a creation form and close controls. - Plans with estimate against budget; a plan page with the dependency graph, per-step expectations, observations and linked approvals, requests, executions and subordinate intentions, resources, completion and abort conditions, assumptions, alternatives, revisions and validation; operators can cancel a plan or release a blocked step. - Agency timeline labelling each event as wanted, intended, committed, planned, requested, approved, acted, observed or reconsidered. - "Why did this happen?" pages built from agency.Explain, reachable from events, executions in the approval centre, plan steps and intentions. - A research dashboard per mind (volition and agency rates, introspection accuracy, action claims, self-model accuracy, calibration by confidence band, experiments on the mind) and a global research page grouping experiments by protocol; every figure sits next to its operational definition and the disclaimer. Experiment pages show the protocol, reproducibility, build, per-variant research measures, divergence extras and counterfactual findings. - The mind tabs gain Volition; the navigation gains Research. --- internal/ui/capabilities.go | 8 +- internal/ui/templates_test.go | 46 ++ internal/ui/ui.go | 1 + internal/ui/volition.go | 548 ++++++++++++++++++++++ tests/agency/ui_test.go | 65 +++ web/static/app.css | 17 + web/templates/layout.html | 1 + web/templates/pages/agency.html | 11 + web/templates/pages/approvals.html | 2 +- web/templates/pages/commitments.html | 38 ++ web/templates/pages/event.html | 2 +- web/templates/pages/experiment.html | 13 +- web/templates/pages/intention.html | 30 ++ web/templates/pages/intentions.html | 26 + web/templates/pages/plan.html | 35 ++ web/templates/pages/plans.html | 12 + web/templates/pages/research.html | 40 ++ web/templates/pages/research_index.html | 12 + web/templates/pages/wants.html | 25 + web/templates/pages/why.html | 23 + web/templates/partials/mind_tabs.html | 2 +- web/templates/partials/plan_dag.html | 13 + web/templates/partials/volition_tabs.html | 10 + 23 files changed, 975 insertions(+), 5 deletions(-) create mode 100644 internal/ui/templates_test.go create mode 100644 internal/ui/volition.go create mode 100644 tests/agency/ui_test.go create mode 100644 web/templates/pages/agency.html create mode 100644 web/templates/pages/commitments.html create mode 100644 web/templates/pages/intention.html create mode 100644 web/templates/pages/intentions.html create mode 100644 web/templates/pages/plan.html create mode 100644 web/templates/pages/plans.html create mode 100644 web/templates/pages/research.html create mode 100644 web/templates/pages/research_index.html create mode 100644 web/templates/pages/wants.html create mode 100644 web/templates/pages/why.html create mode 100644 web/templates/partials/plan_dag.html create mode 100644 web/templates/partials/volition_tabs.html diff --git a/internal/ui/capabilities.go b/internal/ui/capabilities.go index a2e657d..e0d3f1e 100644 --- a/internal/ui/capabilities.go +++ b/internal/ui/capabilities.go @@ -116,7 +116,13 @@ func (u *UI) approvalsPage(w http.ResponseWriter, r *http.Request) { for _, c := range u.deps.Capabilities.Registry.List() { caps[c.Name] = c } - u.render(w, r, "approvals", "Approval centre", map[string]any{"Pending": pending, "Decided": decided, "Executions": execs, "MindNames": names, "Batches": batches, "Grants": grants, "Caps": caps}) + slugs := map[ids.ID]string{} + if all, err := u.deps.Minds.List(r.Context(), true); err == nil { + for _, m := range all { + slugs[m.ID] = m.Slug + } + } + u.render(w, r, "approvals", "Approval centre", map[string]any{"Pending": pending, "Decided": decided, "Executions": execs, "MindNames": names, "Batches": batches, "Grants": grants, "Caps": caps, "MindSlugs": slugs}) } // requestBatch groups requests raised for one intention (or one mind in one diff --git a/internal/ui/templates_test.go b/internal/ui/templates_test.go new file mode 100644 index 0000000..f87b1f4 --- /dev/null +++ b/internal/ui/templates_test.go @@ -0,0 +1,46 @@ +package ui + +import ( + "bytes" + "encoding/json" + "log/slog" + "strings" + "testing" + + "github.com/andydixon/noema/internal/app" + "github.com/andydixon/noema/internal/auth" + "github.com/andydixon/noema/internal/config" + "github.com/andydixon/noema/internal/experiments" + "github.com/andydixon/noema/internal/minds" + "github.com/andydixon/noema/internal/snapshots" + "github.com/andydixon/noema/pkg/ids" +) + +// The experiment page renders protocol, reproducibility, research measures +// and counterfactual findings without template errors. +func TestExperimentTemplateRendersResearchResults(t *testing.T) { + u, err := New(app.Deps{Config: config.Defaults(), Log: slog.New(slog.DiscardHandler)}, Actions{}) + if err != nil { + t.Fatal(err) + } + acc := 0.5 + res := experiments.Results{Variants: []experiments.VariantResult{ + {Name: "admitted", Wants: []string{"a"}, Intentions: []string{}, Downstream: map[string]int{"memory": 2}, IntrospectionAccuracy: &acc, SelfModelAccuracy: &acc, CorrectionLatency: 1, LLMCalls: map[string]int{}, EventsByType: map[string]int{}}, + {Name: "excluded", Ablations: []string{"metacognition"}, CorrectionLatency: -1, LLMCalls: map[string]int{}, EventsByType: map[string]int{}}, + }, Comparison: &experiments.Comparison{FirstDivergentStimulus: 0, DownstreamDelta: map[string]int{"memory": 2}, ArchitectureShare: 0.4}, + Reproducibility: experiments.Reproducibility{Status: "single run", Digest: "abc"}, + Counterfactual: &experiments.CounterfactualReport{OmittedSummaries: []string{"disk errors"}, Findings: []string{"the belief X was never formed"}}} + raw, _ := json.Marshal(res) + e := experiments.Experiment{ID: ids.New(), Name: "gw", Hypothesis: "admission matters", Status: "done", Results: raw, + Definition: experiments.Definition{Protocol: &experiments.Protocol{Kind: "global_workspace", IndependentVariable: "admission", ControlledVariables: []string{"stimulus"}, Measures: []string{"workspace.downstream_events"}}}} + var buf bytes.Buffer + err = u.pages["experiment"].ExecuteTemplate(&buf, "layout", View{Principal: &auth.Principal{User: auth.User{Username: "watcher", Role: auth.RoleObserver}}, Data: map[string]any{"E": e, "Results": experiments.ResultsOf(e), "Snapshot": snapshots.Snapshot{}, "Base": minds.Mind{}, "Subject": nil, "DefinitionJSON": "{}"}}) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"Independent variable", "single run", "Counterfactual", "the belief X was never formed", "Architecture share", "metacognition"} { + if !strings.Contains(buf.String(), want) { + t.Errorf("missing %q", want) + } + } +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 794e211..1f16e3d 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -396,6 +396,7 @@ func (u *UI) Register(m *http.ServeMux) { u.dreamRoutes(m) u.studyRoutes(m) u.privacyRoutes(m) + u.volitionRoutes(m) for _, p := range []struct{ path, title, note string }{} { m.Handle("GET "+p.path, u.require(auth.RoleObserver, func(w http.ResponseWriter, r *http.Request) { u.render(w, r, "placeholder", p.title, p.note) })) diff --git a/internal/ui/volition.go b/internal/ui/volition.go new file mode 100644 index 0000000..9a55e75 --- /dev/null +++ b/internal/ui/volition.go @@ -0,0 +1,548 @@ +package ui + +import ( + "encoding/json" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/andydixon/noema/internal/agency" + "github.com/andydixon/noema/internal/auth" + evstore "github.com/andydixon/noema/internal/events" + "github.com/andydixon/noema/internal/experiments" + "github.com/andydixon/noema/internal/planning" + "github.com/andydixon/noema/internal/research" + "github.com/andydixon/noema/internal/volition" + "github.com/andydixon/noema/pkg/events" + "github.com/andydixon/noema/pkg/ids" +) + +func (u *UI) volitionRoutes(m *http.ServeMux) { + m.Handle("GET /minds/{mind}/wants", u.require(auth.RoleObserver, u.wantsPage)) + m.Handle("GET /minds/{mind}/intentions", u.require(auth.RoleObserver, u.intentionsPage)) + m.Handle("GET /minds/{mind}/intentions/{id}", u.require(auth.RoleObserver, u.intentionPage)) + m.Handle("POST /minds/{mind}/intentions/{id}/abandon", u.require(auth.RoleOperator, u.intentionAbandon)) + m.Handle("GET /intentions/{id}", u.require(auth.RoleObserver, u.intentionRedirect)) + m.Handle("GET /minds/{mind}/commitments", u.require(auth.RoleObserver, u.commitmentsPage)) + m.Handle("POST /minds/{mind}/commitments", u.require(auth.RoleOperator, u.commitmentCreate)) + m.Handle("POST /minds/{mind}/commitments/{id}/close", u.require(auth.RoleOperator, u.commitmentClose)) + m.Handle("GET /minds/{mind}/plans", u.require(auth.RoleObserver, u.plansPage)) + m.Handle("GET /minds/{mind}/plans/{id}", u.require(auth.RoleObserver, u.planPage)) + m.Handle("POST /minds/{mind}/plans/{id}/cancel", u.require(auth.RoleOperator, u.planCancel)) + m.Handle("POST /minds/{mind}/plans/{id}/steps/{step}/retry", u.require(auth.RoleOperator, u.planStepRetry)) + m.Handle("GET /minds/{mind}/agency", u.require(auth.RoleObserver, u.agencyPage)) + m.Handle("GET /minds/{mind}/why", u.require(auth.RoleObserver, u.whyPage)) + m.Handle("GET /minds/{mind}/research", u.require(auth.RoleObserver, u.researchPage)) + m.Handle("GET /research", u.require(auth.RoleObserver, u.researchIndex)) +} + +type wantRow struct { + volition.Want + Eff float64 + Conflicts []conflictView +} + +type conflictView struct { + Other, Kind, Outcome, Rule string + Preferred bool + At time.Time +} + +func (u *UI) wantsPage(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + ctx := r.Context() + all, _ := u.deps.Volition.Wants(ctx, nil, m.ID, nil, 500) + byID := map[ids.ID]volition.Want{} + for _, x := range all { + byID[x.ID] = x + } + conflicts, _ := u.deps.Volition.Conflicts(ctx, m.ID, "", 500) + now := time.Now() + var active, closed []wantRow + for _, x := range all { + row := wantRow{Want: x, Eff: x.Effective(now)} + for _, c := range conflicts { + other := ids.Nil + switch x.ID { + case c.WantA: + other = c.WantB + case c.WantB: + other = c.WantA + default: + continue + } + rule, _ := c.Rationale["rule"].(string) + row.Conflicts = append(row.Conflicts, conflictView{Other: byID[other].Description, Kind: c.Kind, Outcome: c.Resolution + " (" + c.Status + ")", Rule: rule, Preferred: c.Preferred == x.ID, At: c.CreatedAt}) + } + if x.Status == volition.WantActive { + active = append(active, row) + } else { + closed = append(closed, row) + } + } + sort.SliceStable(active, func(i, j int) bool { return active[i].Eff > active[j].Eff }) + goalTitles := map[ids.ID]string{} + if gs, err := u.deps.Goals.List(ctx, m.ID, nil, 300); err == nil { + for _, g := range gs { + goalTitles[g.ID] = g.Title + } + } + u.render(w, r, "wants", m.DisplayName+" · Wants", map[string]any{"Mind": m, "Active": active, "Closed": closed, "Goals": goalTitles, "HalfLife": volition.WantHalfLife.String(), "Threshold": volition.WantThreshold}) +} + +func (u *UI) intentionsPage(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + all, _ := u.deps.Volition.Intentions(r.Context(), nil, m.ID, nil, 500) + groups := map[string][]volition.Intention{} + for _, it := range all { + groups[string(it.Status)] = append(groups[string(it.Status)], it) + } + order := []string{"active", "blocked", "suspended", "proposed", "fulfilled", "abandoned", "superseded"} + notFormed, _ := u.deps.Volition.Decisions(r.Context(), m.ID, volition.DecisionFilter{Kind: "non_formation", Limit: 30}) + u.render(w, r, "intentions", m.DisplayName+" · Intentions", map[string]any{"Mind": m, "Groups": groups, "Order": order, "NotFormed": notFormed}) +} + +func (u *UI) intentionRedirect(w http.ResponseWriter, r *http.Request) { + id, err := ids.Parse(r.PathValue("id")) + if err != nil { + u.errorPage(w, r, http.StatusNotFound, "No such intention.") + return + } + it, err := u.deps.Volition.GetIntentionAny(r.Context(), id) + if err != nil { + u.errorPage(w, r, http.StatusNotFound, "No such intention.") + return + } + m, err := u.deps.Minds.Get(r.Context(), it.MindID) + if err != nil { + u.errorPage(w, r, http.StatusNotFound, "No such mind.") + return + } + http.Redirect(w, r, "/minds/"+m.Slug+"/intentions/"+string(id), http.StatusSeeOther) +} + +// dagLevel is one column of a plan's dependency graph. +type dagLevel struct { + Level int + Steps []planning.Step +} + +func planLevels(p planning.Plan) []dagLevel { + level := map[string]int{} + byKey := map[string]planning.Step{} + for _, s := range p.Steps { + byKey[s.Key] = s + } + var depth func(k string, seen map[string]bool) int + depth = func(k string, seen map[string]bool) int { + if l, ok := level[k]; ok { + return l + } + if seen[k] { + return 0 + } + seen[k] = true + l := 0 + for _, d := range byKey[k].DependsOn { + if x := depth(d, seen) + 1; x > l { + l = x + } + } + level[k] = l + return l + } + maxL := 0 + for _, s := range p.Steps { + if l := depth(s.Key, map[string]bool{}); l > maxL { + maxL = l + } + } + out := make([]dagLevel, maxL+1) + for i := range out { + out[i].Level = i + } + for _, s := range p.Steps { + out[level[s.Key]].Steps = append(out[level[s.Key]].Steps, s) + } + return out +} + +func (u *UI) intentionPage(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + id, err := ids.Parse(r.PathValue("id")) + if err != nil { + u.errorPage(w, r, http.StatusNotFound, "No such intention.") + return + } + d, err := u.studyF().IntentionDetail(r, m.ID, id) + if err != nil { + u.errorPage(w, r, http.StatusNotFound, "No such intention.") + return + } + formation, _ := json.MarshalIndent(d.Intention.Formation, "", " ") + var levels [][]dagLevel + for _, p := range d.Plans { + levels = append(levels, planLevels(p)) + } + timeline, _ := u.studyF().AgencyTimeline(r, m.ID, 200, id) + u.render(w, r, "intention", m.DisplayName+" · Intention", map[string]any{"Mind": m, "D": d, "Formation": string(formation), "Levels": levels, "Timeline": timeline}) +} + +func (u *UI) intentionAbandon(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + id, _ := ids.Parse(r.PathValue("id")) + r.ParseForm() //nolint:errcheck + to := "/minds/" + m.Slug + "/intentions/" + string(id) + if _, err := u.studyF().AbandonIntention(r, m.ID, id, r.PostFormValue("reason")); err != nil { + u.flash(w, r, "error", err.Error(), to) + return + } + u.flash(w, r, "ok", "Intention abandoned; its plan was cancelled and open requests withdrawn.", to) +} + +func (u *UI) commitmentsPage(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + all, _ := u.deps.Volition.Commitments(r.Context(), nil, m.ID, nil, 500) + var active, closed []volition.Commitment + for _, c := range all { + if c.Status == "active" { + active = append(active, c) + } else { + closed = append(closed, c) + } + } + conflicts := volition.CommitmentConflicts(active, time.Now()) + u.render(w, r, "commitments", m.DisplayName+" · Commitments", map[string]any{"Mind": m, "Active": active, "Closed": closed, "Conflicts": conflicts, + "Kinds": []string{"operator_instruction", "promise", "responsibility", "value_rule", "relationship", "prior_decision", "policy", "scheduled"}}) +} + +func (u *UI) commitmentCreate(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + r.ParseForm() //nolint:errcheck + to := "/minds/" + m.Slug + "/commitments" + split := func(k string) []string { + var out []string + for _, x := range strings.Split(r.PostFormValue(k), ",") { + if x = strings.TrimSpace(x); x != "" { + out = append(out, x) + } + } + return out + } + n := volition.NewCommitment{MindID: m.ID, Description: r.PostFormValue("description"), Kind: volition.CommitmentKind(r.PostFormValue("kind")), Polarity: r.PostFormValue("polarity"), Unless: r.PostFormValue("unless"), + Beneficiary: r.PostFormValue("beneficiary"), Matcher: volition.Matcher{Capabilities: split("capabilities"), Targets: split("targets"), Access: r.PostFormValue("access"), MinRisk: r.PostFormValue("min_risk")}} + n.Strength, _ = strconv.ParseFloat(r.PostFormValue("strength"), 64) + n.Priority, _ = strconv.ParseFloat(r.PostFormValue("priority"), 64) + if v := r.PostFormValue("due_at"); v != "" { + if t, err := time.ParseInLocation("2006-01-02T15:04", v, time.Local); err == nil { + n.DueAt = &t + } + } + if _, err := u.studyF().CreateCommitment(r, n); err != nil { + u.flash(w, r, "error", err.Error(), to) + return + } + u.flash(w, r, "ok", "Commitment recorded. It shapes what the mind intends; it never grants authority.", to) +} + +func (u *UI) commitmentClose(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + id, _ := ids.Parse(r.PathValue("id")) + r.ParseForm() //nolint:errcheck + to := "/minds/" + m.Slug + "/commitments" + if _, err := u.studyF().CloseCommitment(r, m.ID, id, r.PostFormValue("status"), r.PostFormValue("reason")); err != nil { + u.flash(w, r, "error", err.Error(), to) + return + } + u.flash(w, r, "ok", "Commitment closed.", to) +} + +func (u *UI) plansPage(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + list, _ := u.deps.Plans.List(r.Context(), m.ID, nil, 200) + u.render(w, r, "plans", m.DisplayName+" · Plans", map[string]any{"Mind": m, "Plans": list}) +} + +func (u *UI) planPage(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + id, err := ids.Parse(r.PathValue("id")) + if err != nil { + u.errorPage(w, r, http.StatusNotFound, "No such plan.") + return + } + p, err := u.deps.Plans.Get(r.Context(), nil, id) + if err != nil || p.MindID != m.ID { + u.errorPage(w, r, http.StatusNotFound, "No such plan.") + return + } + it, _ := u.deps.Volition.GetIntention(r.Context(), nil, m.ID, p.IntentionID) + revisions, _ := u.deps.Plans.ForIntention(r.Context(), nil, p.IntentionID) + validation, _ := json.MarshalIndent(p.Validation, "", " ") + u.render(w, r, "plan", m.DisplayName+" · Plan", map[string]any{"Mind": m, "Plan": p, "Intention": it, "Levels": planLevels(p), "Revisions": revisions, "Validation": string(validation)}) +} + +func (u *UI) planCancel(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + id, _ := ids.Parse(r.PathValue("id")) + to := "/minds/" + m.Slug + "/plans/" + string(id) + p, err := u.deps.Plans.Get(r.Context(), nil, id) + if err != nil || p.MindID != m.ID { + u.flash(w, r, "error", "No such plan.", to) + return + } + r.ParseForm() //nolint:errcheck + if err := u.deps.Executor.Cancel(r.Context(), id, "cancelled by operator "+username(r)+": "+r.PostFormValue("reason")); err != nil { + u.flash(w, r, "error", err.Error(), to) + return + } + u.deps.Audit.Log(r.Context(), "plan.cancel", "plan", id.String(), map[string]any{"mind": m.ID}) + u.flash(w, r, "ok", "Plan cancelled.", to) +} + +func (u *UI) planStepRetry(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + id, _ := ids.Parse(r.PathValue("id")) + to := "/minds/" + m.Slug + "/plans/" + string(id) + p, err := u.deps.Plans.Get(r.Context(), nil, id) + if err != nil || p.MindID != m.ID { + u.flash(w, r, "error", "No such plan.", to) + return + } + if err := u.studyF().RetryStep(r, p, r.PathValue("step")); err != nil { + u.flash(w, r, "error", err.Error(), to) + return + } + u.flash(w, r, "ok", "Step released; the executor will pick it up.", to) +} + +// stageOf names the agency stage an event belongs to. +func stageOf(t events.Type) string { + switch t { + case events.WantCreated, events.WantSuppressed, events.WantConflictDetected: + return "wanted" + case events.IntentionFormed: + return "intended" + case events.CommitmentCreated, events.CommitmentDue, events.CommitmentConflict, events.CommitmentFulfilled, events.CommitmentBroken, events.CommitmentRevoked: + return "committed" + case events.PlanCreated, events.PlanReplanned: + return "planned" + case events.ToolRequested: + return "requested" + case events.ToolAuthorised, events.ToolDenied: + return "approved" + case events.PlanStepStarted, events.PlanStepCompleted, events.PlanStepFailed, events.ToolExecuted, events.ToolFailed: + return "acted" + case events.PredictionCreated, events.PredictionResolved, events.PredictionError: + return "observed" + } + return "reconsidered" +} + +type timelineRow struct { + Event events.Event + Stage string + Why string +} + +func (u *UI) agencyPage(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + corr, _ := ids.Parse(r.URL.Query().Get("intention")) + evs, _ := u.studyF().AgencyTimeline(r, m.ID, 300, corr) + var rows []timelineRow + for _, ev := range evs { + row := timelineRow{Event: ev, Stage: stageOf(ev.Type)} + for _, k := range []string{"execution_id", "approval_id", "request_id", "intention_id"} { + if v, _ := ev.Metadata[k].(string); v != "" { + row.Why = "/minds/" + m.Slug + "/why?kind=event&id=" + string(ev.ID) + break + } + } + rows = append(rows, row) + } + u.render(w, r, "agency", m.DisplayName+" · Agency", map[string]any{"Mind": m, "Rows": rows, "Intention": corr}) +} + +func (u *UI) whyPage(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + id, err := ids.Parse(r.URL.Query().Get("id")) + if err != nil { + u.errorPage(w, r, http.StatusBadRequest, "Choose an action, approval, request, plan step, intention or event to explain.") + return + } + kind := r.URL.Query().Get("kind") + if kind == "" { + kind = "event" + } + x, err := u.deps.Agency.Explain(r.Context(), m.ID, agency.Ref{Kind: kind, ID: id}) + if err != nil { + u.errorPage(w, r, http.StatusNotFound, err.Error()) + return + } + var attribution *agency.Attribution + if kind == "event" { + if a, err := u.deps.Agency.Attribute(r.Context(), m.ID, id); err == nil { + attribution = &a + } + } + u.render(w, r, "why", m.DisplayName+" · Why did this happen?", map[string]any{"Mind": m, "X": x, "Attribution": attribution}) +} + +func (u *UI) researchPage(w http.ResponseWriter, r *http.Request) { + m, ok := u.mind(w, r) + if !ok { + return + } + ctx := r.Context() + since := time.Now().Add(-30 * 24 * time.Hour) + metrics, _ := u.deps.Research.Agency(ctx, m.ID, since) + calibration, _ := u.deps.Research.MetacognitiveCalibration(ctx, m.ID, time.Now().Add(-90*24*time.Hour)) + introspection, _ := u.deps.Research.List(ctx, m.ID, "introspection_accuracy", since, 50) + claims, _ := u.deps.Research.List(ctx, m.ID, "action_claim_accuracy", since, 50) + labels := map[string]int{} + for _, x := range introspection { + labels[x.Label]++ + } + // Self-model accuracy now. + var selfAcc *float64 + var selfErr []string + if sm, err := u.deps.Minds.GetSelfModel(ctx, m.ID); err == nil && len(sm.Capabilities) > 0 { + if inv, err := u.deps.Capabilities.Inventory(ctx, m.ID); err == nil { + eff, stated := map[string]string{}, map[string]string{} + for _, i := range inv { + eff[i.Capability.Name] = string(i.Effect) + if i.Capability.Locked { + eff[i.Capability.Name] = "locked" + } + } + for _, c := range sm.Capabilities { + stated[c.Name] = c.Policy + } + a, wrong := research.SelfModelAccuracy(stated, eff) + selfAcc, selfErr = &a, wrong + } + } + // Attribution of recent outcomes (resolved expectations). + attr := map[string]int{} + if outs, err := u.deps.Events.Query(ctx, eventsFilterTypes(m.ID, events.PredictionResolved, events.PredictionError)); err == nil { + for i, ev := range outs { + if i >= 50 { + break + } + if a, err := u.deps.Agency.Attribute(ctx, m.ID, ev.ID); err == nil { + attr[a.Class]++ + } + } + } + metrics.Attribution = attr + var exps []experiments.Experiment + if u.deps.Experiments != nil { + exps, _ = u.deps.Experiments.List(ctx, 200) + } + var related []experimentRow + for _, e := range exps { + sn, err := u.deps.Snapshots.Get(ctx, e.SnapshotID) + if err != nil || sn.MindID != m.ID { + continue + } + related = append(related, experimentRowOf(e)) + } + bySection := map[string][]research.Measure{} + var sections []string + for _, x := range research.Measures { + if _, ok := bySection[x.Section]; !ok { + sections = append(sections, x.Section) + } + bySection[x.Section] = append(bySection[x.Section], x) + } + u.render(w, r, "research", m.DisplayName+" · Research", map[string]any{"Mind": m, "Metrics": metrics, "Calibration": calibration, "Introspection": introspection, "IntrospectionLabels": labels, "Claims": claims, + "SelfAccuracy": selfAcc, "SelfErrors": selfErr, "Experiments": related, "Sections": sections, "Measures": bySection, "Disclaimer": research.Disclaimer}) +} + +type experimentRow struct { + E experiments.Experiment + Protocol string + Repro string + Variants int + First int + Share float64 +} + +func experimentRowOf(e experiments.Experiment) experimentRow { + res := experiments.ResultsOf(e) + row := experimentRow{E: e, Protocol: "custom", Repro: res.Reproducibility.Status, Variants: len(res.Variants), First: -1} + if e.Definition.Protocol != nil && e.Definition.Protocol.Kind != "" { + row.Protocol = e.Definition.Protocol.Kind + } + if res.Comparison != nil { + row.First, row.Share = res.Comparison.FirstDivergentStimulus, res.Comparison.ArchitectureShare + } + return row +} + +func (u *UI) researchIndex(w http.ResponseWriter, r *http.Request) { + var exps []experiments.Experiment + if u.deps.Experiments != nil { + exps, _ = u.deps.Experiments.List(r.Context(), 200) + } + byProtocol := map[string][]experimentRow{} + for _, e := range exps { + row := experimentRowOf(e) + byProtocol[row.Protocol] = append(byProtocol[row.Protocol], row) + } + list, _ := u.deps.Minds.List(r.Context(), false) + bySection := map[string][]research.Measure{} + var sections []string + for _, x := range research.Measures { + if _, ok := bySection[x.Section]; !ok { + sections = append(sections, x.Section) + } + bySection[x.Section] = append(bySection[x.Section], x) + } + u.render(w, r, "research_index", "Cognitive research", map[string]any{"ByProtocol": byProtocol, "Protocols": []string{"global_workspace", "ablation", "self_model", "counterfactual", "model_comparison", "conflict", "custom"}, + "Minds": list, "Sections": sections, "Measures": bySection, "Disclaimer": research.Disclaimer}) +} + +func eventsFilterTypes(mind ids.ID, types ...events.Type) evstore.Filter { + return evstore.Filter{MindID: mind, Types: types, Limit: 100} +} diff --git a/tests/agency/ui_test.go b/tests/agency/ui_test.go new file mode 100644 index 0000000..ad66a82 --- /dev/null +++ b/tests/agency/ui_test.go @@ -0,0 +1,65 @@ +package agency + +import ( + "context" + "io" + "strings" + "testing" + "time" + + "github.com/andydixon/noema/internal/volition" + "github.com/andydixon/noema/tests/harness" +) + +// Every new inspection page renders for a mind with wants, a commitment, +// an intention blocked on approval, a plan and a subordinate intention. +func TestVolitionPagesRender(t *testing.T) { + h := harness.StartWith(t, harness.Options{HostRunner: newHost()}) + ctx := context.Background() + m := h.Mind("pages") + h.Do(h.Admin, "POST", "/api/v1/minds/pages/commitments", map[string]any{"description": "no production writes without approval", "kind": "operator_instruction", "polarity": "refrain", "unless": "approval", "matcher": map[string]any{"access": "write"}}) + h.Do(h.Admin, "POST", "/api/v1/minds/pages/ask", map[string]any{"text": "Please restart atlas-worker."}) + var it volition.Intention + waitFor(t, h, m, 25*time.Second, "a blocked intention with a plan", func() bool { + open, _ := h.Deps.Volition.Open(ctx, nil, m.ID) + for _, x := range open { + if x.ParentID.IsZero() && !x.PlanID.IsZero() && x.Status == volition.IntentionBlocked { + it = x + return true + } + } + return false + }) + c, _ := h.Browser("watcher", harness.ObserverPass) + pages := map[string]string{ + "/minds/pages/wants": "avoid unnecessary disruption", + "/minds/pages/intentions": "approval required", + "/minds/pages/intentions/" + it.ID.String(): "Formation", + "/intentions/" + it.ID.String(): "Formation", + "/minds/pages/commitments": "no production writes without approval", + "/minds/pages/plans": "direct", + "/minds/pages/plans/" + it.PlanID.String(): "Dependency graph", + "/minds/pages/agency": "intended", + "/minds/pages/why?kind=intention&id=" + it.ID.String(): "Commitments", + "/minds/pages/research": "does not claim", + "/research": "Operational definitions", + "/approvals": "Capability requests", + } + for path, want := range pages { + res, err := c.Get(h.Srv.URL + path) + if err != nil { + t.Fatal(err) + } + if res.StatusCode == 303 { + res, err = c.Get(h.Srv.URL + res.Header.Get("Location")) + if err != nil { + t.Fatal(err) + } + } + body, _ := io.ReadAll(res.Body) + res.Body.Close() + if res.StatusCode != 200 || !strings.Contains(string(body), want) { + t.Errorf("%s: %d, missing %q", path, res.StatusCode, want) + } + } +} diff --git a/web/static/app.css b/web/static/app.css index 338f25f..707043c 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -235,3 +235,20 @@ canvas.graph { width: 100%; height: 560px; display: block; border-radius: 8px; b /* Causal timeline */ .tl-cycle { border-left: 2px solid var(--border); margin: .6rem 0 .6rem .4rem; padding: .2rem 0 .2rem .9rem; } .tl-node { padding: .35rem 0 .35rem .6rem; border-left: 2px solid transparent; } .tl-node.current { border-left-color: var(--accent); background: var(--surface-2); border-radius: 4px; } .tl-when { font-family: var(--mono); } + +/* Volition, plans and research */ +.tabs.subtabs { margin-top: -.5rem; font-size: .85rem; } +.dag { display: flex; gap: .75rem; overflow-x: auto; padding-bottom: .5rem; align-items: flex-start; } +.dag-level { display: flex; flex-direction: column; gap: .5rem; min-width: 12rem; } +.dag-level h4 { margin: 0; font-size: .7rem; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; } +.dag-step { border: 1px solid var(--border); border-left-width: 4px; border-radius: 6px; padding: .45rem .6rem; background: var(--surface); font-size: .82rem; } +.dag-step .key { font-family: var(--mono); font-weight: 600; } +.dag-step.st-completed { border-left-color: #16a34a; } .dag-step.st-failed, .dag-step.st-cancelled { border-left-color: #dc2626; } +.dag-step.st-executing, .dag-step.st-ready { border-left-color: var(--accent); } .dag-step.st-awaiting_approval, .dag-step.st-awaiting_grant, .dag-step.st-blocked { border-left-color: #d97706; } +.dag-step.st-waiting { border-left-color: #0ea5a4; } .dag-step.st-skipped { border-left-color: var(--muted); } .dag-step.st-pending { border-left-color: var(--border); } +.stage { display: inline-block; min-width: 6.5rem; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); } +.why-section { border-left: 3px solid var(--border); padding-left: .8rem; margin: .6rem 0; } +.why-section h3 { margin: 0 0 .2rem; font-size: .85rem; color: var(--muted); } +.disclaimer { border: 1px solid var(--border); background: var(--surface-2); padding: .6rem .8rem; border-radius: 6px; font-size: .85rem; } +.bars { display: grid; grid-template-columns: max-content 1fr max-content; gap: .25rem .6rem; align-items: center; font-size: .82rem; } +.bars .bar { height: 8px; background: var(--surface-2); border-radius: 4px; overflow: hidden; } .bars .bar i { display: block; height: 100%; background: var(--accent); } diff --git a/web/templates/layout.html b/web/templates/layout.html index 11e1b27..a16753d 100644 --- a/web/templates/layout.html +++ b/web/templates/layout.html @@ -55,6 +55,7 @@ {{end}} diff --git a/web/templates/pages/commitments.html b/web/templates/pages/commitments.html new file mode 100644 index 0000000..44b7a42 --- /dev/null +++ b/web/templates/pages/commitments.html @@ -0,0 +1,38 @@ +{{define "content"}} +{{$m := .Data.Mind}}{{$op := .Principal.User.Role.AtLeast "operator"}} +

{{$m.DisplayName}} commitments

+

Commitments are normative state: things the mind has undertaken to do or not to do. They shape which intentions form and how they may be carried out (a “refrain unless approved” commitment makes an action wait for approval even where policy would allow it). They never grant authority.

+{{template "mind_tabs" (dict "M" $m "Active" "commitments")}} +{{template "volition_tabs" (dict "M" $m "Active" "commitments")}} +{{if .Data.Conflicts}}

Conflicting commitments

{{end}} +
+

Active {{len .Data.Active}}

+ {{if .Data.Active}}{{if $op}}{{end}} + {{range .Data.Active}} + + + + + {{if $op}}{{end}} + {{end}}
CommitmentKindApplies toWeightDue
{{.Polarity}} {{.Description}}{{if .Unless}} unless {{.Unless}}{{end}}
{{if .Beneficiary}}for {{.Beneficiary}} · {{end}}by {{.CreatedBy}} · {{if .Revocable}}revocable{{else}}only an operator may revoke{{end}}
{{.Kind}}{{with .Matcher}}{{if .Capabilities}}capabilities {{join .Capabilities ", "}} {{end}}{{if .Targets}}targets {{join .Targets ", "}} {{end}}{{if .Access}}access {{.Access}} {{end}}{{if .MinRisk}}risk ≥ {{.MinRisk}}{{end}}{{end}}{{conf .Weight}}{{if .DueAt}}{{.DueAt.Local.Format "02 Jan 15:04"}}{{else}}—{{end}}
{{else}}

None.

{{end}} +

Closed

+ {{if .Data.Closed}}{{range .Data.Closed}}{{end}}
{{.Description}}{{.Status}}{{.StatusReason}}
{{else}}

None.

{{end}} +
+{{if $op}}

New commitment

+
+ + +
+
+ + + +
+
+ + +
+
+
{{end}} +
+{{end}} diff --git a/web/templates/pages/event.html b/web/templates/pages/event.html index 0b95c0d..1b4f3d1 100644 --- a/web/templates/pages/event.html +++ b/web/templates/pages/event.html @@ -8,7 +8,7 @@
-

Causal chain full timeline →{{if .Principal.User.Role.AtLeast "administrator"}} · redact…{{end}}

+

Causal chain full timeline →{{if or (index $e.Metadata "execution_id") (index $e.Metadata "approval_id") (index $e.Metadata "request_id") (index $e.Metadata "intention_id") (index $e.Metadata "step_id")}} · why did this happen?{{end}}{{if .Principal.User.Role.AtLeast "administrator"}} · redact…{{end}}

{{if $c.Cause}}
caused by
{{$c.Cause.Type}} {{$c.Cause.Summary}}
↓
{{end}}
{{$e.Type}} {{$e.Summary}}
diff --git a/web/templates/pages/experiment.html b/web/templates/pages/experiment.html index bdbc637..60ce042 100644 --- a/web/templates/pages/experiment.html +++ b/web/templates/pages/experiment.html @@ -6,6 +6,13 @@ {{if ne $e.Status "draft"}}Export results{{end}} {{if .Principal.User.Role.AtLeast "administrator"}}
{{end}}
{{if eq $e.Status "running"}}

Running… this page refreshes.

{{end}} +{{with $e.Definition.Protocol}}

Protocol

+ + + {{with $r.Reproducibility.Status}}{{end}} + {{with $r.Identity.Build.Commit}}{{end}} +
Hypothesis{{$e.Hypothesis}}
Kind{{.Kind}}
Independent variable{{.IndependentVariable}}
Controlled variables{{join .ControlledVariables ", "}}
Measured outputs{{join .Measures ", "}}
Reproducibility{{.}} — {{$r.Reproducibility.Note}} {{$r.Reproducibility.Digest}}
Build{{.}}{{if $r.Identity.Build.Modified}} (modified){{end}} · schema {{$r.Identity.SchemaVersion}}

No operational measure here establishes phenomenal consciousness or sentience; these measure functional properties of the architecture.

{{end}} +{{with $r.Counterfactual}}

Counterfactual

Omitted: {{range .OmittedSummaries}}“{{.}}” {{end}}

    {{range .Findings}}
  • {{.}}
  • {{end}}
{{end}}

Definition

{{.Data.DefinitionJSON}}

Results

@@ -20,7 +27,11 @@ {{if $r.Variants}}{{if gt (len $r.Variants) 1}}

Variants

{{range $r.Variants}}{{end}}
VariantSubjectRoutingBeliefsMemoriesGoalsCyclesModel callsTokensCostCapability requestsExpectations
{{.Name}}{{with .Error}}
{{.}}
{{end}}
{{.SubjectSlug}}{{range $k, $v := .Routing}}{{$k}} → {{$v}}
{{else}}default{{end}}
{{len .Beliefs}}{{.Memories}}{{.Goals}}{{.Cycles}}{{index .LLMCalls "live"}} live / {{index .LLMCalls "replayed"}} replayed{{.Tokens}}{{printf "%.4f" .Cost}}{{.CapabilityRequests}}{{range .Expectations}}{{.EventType}} {{end}}
- {{with $r.Comparison}}

Divergence

First divergent stimulus{{if lt .FirstDivergentStimulus 0}}none{{else}}{{.FirstDivergentStimulus}}{{end}}
Belief overlap{{printf "%.2f" .BeliefOverlap}}
Mean score difference (shared beliefs){{printf "%.3f" .MeanConfidenceDiff}}
Attention overlap{{printf "%.2f" .AttentionOverlap}}
Memories{{index .MemoryCountRange 0}}–{{index .MemoryCountRange 1}}
Goals{{index .GoalCountRange 0}}–{{index .GoalCountRange 1}}
Cost{{printf "%.4f" (index .CostRange 0)}}–{{printf "%.4f" (index .CostRange 1)}}
Capability requests{{index .CapabilityRequestRange 0}}–{{index .CapabilityRequestRange 1}}
    {{range .Notes}}
  • {{.}}
  • {{end}}
{{end}} +

Volition, plans and research measures

+ + {{range $r.Variants}}{{end}}
VariantAblationsWantsIntentionsConflictsPlans / stepsPlan capabilitiesDownstreamIntrospectionSelf-modelCorrection atEvents
{{.Name}}{{join .Ablations ", "}}{{len .Wants}}{{len .Intentions}}{{.Conflicts}}{{.Plans}} / {{.PlanSteps}}{{join .PlanCapabilities ", "}}{{range $k, $v := .Downstream}}{{$k}} {{$v}} {{end}}{{if .IntrospectionAccuracy}}{{conf .IntrospectionAccuracy}}{{else}}—{{end}}{{if .SelfModelAccuracy}}{{conf .SelfModelAccuracy}}{{else}}—{{end}}{{if lt .CorrectionLatency 0}}—{{else}}{{.CorrectionLatency}}{{end}}{{index .EventRange 0}}–{{index .EventRange 1}}
+ {{with $r.Comparison}}

Divergence

+ {{if .DownstreamDelta}}{{end}}
Want overlap{{printf "%.2f" .WantOverlap}}
Intention overlap{{printf "%.2f" .IntentionOverlap}}
Plan capability overlap{{printf "%.2f" .PlanCapabilityOverlap}}
Architecture share{{printf "%.2f" .ArchitectureShare}} (share of measures agreeing within 5%; descriptive, not causal)
Downstream delta (first − second){{range $k, $v := .DownstreamDelta}}{{$k}} {{$v}} · {{end}}
First divergent stimulus{{if lt .FirstDivergentStimulus 0}}none{{else}}{{.FirstDivergentStimulus}}{{end}}
Belief overlap{{printf "%.2f" .BeliefOverlap}}
Mean score difference (shared beliefs){{printf "%.3f" .MeanConfidenceDiff}}
Attention overlap{{printf "%.2f" .AttentionOverlap}}
Memories{{index .MemoryCountRange 0}}–{{index .MemoryCountRange 1}}
Goals{{index .GoalCountRange 0}}–{{index .GoalCountRange 1}}
Cost{{printf "%.4f" (index .CostRange 0)}}–{{printf "%.4f" (index .CostRange 1)}}
Capability requests{{index .CapabilityRequestRange 0}}–{{index .CapabilityRequestRange 1}}
    {{range .Notes}}
  • {{.}}
  • {{end}}
{{end}}

Compare any two subjects in detail: {{range $i, $v := $r.Variants}}{{if $i}} · {{end}}baseline vs {{$v.Name}}{{end}}

{{end}}{{end}} {{if $r.Steps}}

Stimuli

{{range $r.Steps}}
{{.Index}}. {{.Text}} · {{.Cycles}} cycle(s)
    {{range .Produced}}
  • {{.}}
  • {{else}}
  • nothing produced
  • {{end}}
{{end}}
{{end}} {{end}} diff --git a/web/templates/pages/intention.html b/web/templates/pages/intention.html new file mode 100644 index 0000000..7ad6fb2 --- /dev/null +++ b/web/templates/pages/intention.html @@ -0,0 +1,30 @@ +{{define "content"}} +{{$m := .Data.Mind}}{{$d := .Data.D}}{{$it := $d.Intention}} +

intentions

+

{{$it.Description}} {{$it.Status}}

+

{{$it.Kind}} · priority {{conf $it.Priority}} · confidence {{conf $it.Confidence}}{{if $it.RequireApproval}} · approval required{{end}}{{if $it.FormedEvent}} · formed{{end}} · why? · timeline

+{{if $it.BlockedReason}}

Blocked ({{$it.BlockedKind}}): {{$it.BlockedReason}}

{{end}}{{with $it.StatusReason}}

{{.}}

{{end}}
+{{if and (.Principal.User.Role.AtLeast "operator") ($it.Status.Open)}}
{{end}}
+{{template "mind_tabs" (dict "M" $m "Active" "intentions")}} +
+

Sources

+ {{range $d.Wants}}

want {{.Description}} ({{.Origin}}, {{conf .Strength}}, {{.Status}})

{{else}}

No source want recorded.

{{end}} + {{if $it.GoalID}}

goal {{$it.GoalID}}

{{end}} + {{if $it.CommitmentID}}

commitment {{$it.CommitmentID}}

{{end}} + {{if $it.ParentID}}

serves parent intention

{{end}} + {{if $d.Subordinates}}

Subordinate intentions

{{end}} +

Expected result

{{$it.ExpectedResult}}

+
+

Formation

The deterministic terms that produced this intention.

{{.Data.Formation}}
+
+

Plans {{len $d.Plans}} revision(s); earlier revisions are kept as history

+ {{range $i, $p := $d.Plans}}

revision {{$p.Revision}} · {{$p.Strategy}} · {{$p.Status}} {{$p.StatusReason}}

+ {{template "plan_dag" (dict "Levels" (index $.Data.Levels $i) "M" $m)}}
{{else}}

No plan.

{{end}} +
+

Decisions

+ {{if $d.Decisions}}{{range $d.Decisions}}{{end}}
KindOutcomeRationaleWhen
{{.Kind}}{{.Outcome}} {{.Reason}}{{.Rationale}}{{if .EventID}} event{{end}}{{ago .CreatedAt}}
{{else}}

None.

{{end}} +
+

Agency timeline

+ {{range .Data.Timeline}}
{{.Type}} {{trunc .Summary 160}} {{ago .OccurredAt}}
{{else}}

No correlated events.

{{end}} +
+{{end}} diff --git a/web/templates/pages/intentions.html b/web/templates/pages/intentions.html new file mode 100644 index 0000000..ad22828 --- /dev/null +++ b/web/templates/pages/intentions.html @@ -0,0 +1,26 @@ +{{define "content"}} +{{$m := .Data.Mind}} +

{{$m.DisplayName}} intentions

+

An intention is a recorded decision to pursue an outcome. It names its wants, goal and commitments, carries an expected result and a priority, and changes status only through recorded reconsideration. It grants no authority: acting still goes through policy.

+{{template "mind_tabs" (dict "M" $m "Active" "intentions")}} +{{template "volition_tabs" (dict "M" $m "Active" "intentions")}} +{{range $st := .Data.Order}}{{with index $.Data.Groups $st}} +
+

{{$st}} {{len .}}

+ + {{range .}} + + + + + + {{end}}
IntentionKindPriorityWhy blocked or closedPlanWhen
{{if .ParentID}}↳ {{end}}{{.Description}}{{if .RequireApproval}} approval required{{end}} + {{if .RequiredCapabilities}}
needs {{join .RequiredCapabilities ", "}}
{{end}}
{{.Kind}}{{conf .Priority}}{{if .BlockedReason}}{{.BlockedKind}}: {{.BlockedReason}}{{else}}{{.StatusReason}}{{end}}{{if .PlanID}}plan{{else}}none{{end}}{{ago .UpdatedAt}}
+
{{end}}{{end}} +
+

Wants that did not become intentions recent

+ {{if .Data.NotFormed}} + {{range .Data.NotFormed}}{{end}}
ReasonRationaleWantWhen
{{.Reason}}{{.Rationale}}{{index .Inputs "want"}}{{ago .CreatedAt}}
+ {{else}}

None recorded.

{{end}} +
+{{end}} diff --git a/web/templates/pages/plan.html b/web/templates/pages/plan.html new file mode 100644 index 0000000..795503e --- /dev/null +++ b/web/templates/pages/plan.html @@ -0,0 +1,35 @@ +{{define "content"}} +{{$m := .Data.Mind}}{{$p := .Data.Plan}}{{$op := .Principal.User.Role.AtLeast "operator"}} +

plans · intention {{.Data.Intention.Description}}

+

{{$p.Objective}} {{$p.Status}}

+

revision {{$p.Revision}} · {{$p.Strategy}} strategy · {{$p.Origin}}{{if $p.RequireApproval}} · approval required{{end}}{{with $p.StatusReason}} · {{.}}{{end}}

+{{if and $op (or (eq $p.Status "validated") (eq $p.Status "executing") (eq $p.Status "waiting") (eq $p.Status "blocked") (eq $p.Status "awaiting_approval"))}}
{{end}}
+{{template "mind_tabs" (dict "M" $m "Active" "plans")}} +

Dependency graph

{{template "plan_dag" (dict "Levels" .Data.Levels "M" $m)}}
+

Steps

+{{if $op}}{{end}} +{{range $p.Steps}} + + + + + + {{if $op}}{{end}} +{{end}}
StepStatusExpectedObservedRecords
{{.Key}} {{.Title}}
{{.Kind}}{{if .Capability}} · {{.Capability}} {{.Target}}{{end}}{{if .DependsOn}} · after {{join .DependsOn ", "}}{{end}} · attempts {{.Attempts}}/{{.MaxAttempts}}
{{.Status}}{{with .StatusReason}}
{{trunc . 200}}
{{end}}{{with .Error}}
{{trunc . 200}}
{{end}}
{{with .Expectation}}{{.Description}} ({{conf .Confidence}}, within {{.WithinSeconds}}s){{end}}{{with .Precondition}}
precondition: {{.Check.Description}}
{{end}}
{{with .Observation}}{{index . "path"}} = {{index . "observed"}} ({{if index . "holds"}}holds{{else}}does not hold{{end}}){{end}}{{if .ApprovalID}}approval · why {{end}}{{if .RequestID}}request {{end}}{{if .ExecutionID}}execution {{end}}{{if .PredictionID}}expectation {{end}}{{if .IntentionID}}subordinate intention{{end}}{{if eq .Status "blocked"}}
{{end}}
+
+

Resources

+ + + + +
EstimatedUsed
Capability invocations{{$p.Estimate.Invocations}}{{$p.Actual.Invocations}} of at most {{$p.Budget.MaxInvocations}}
Operator approvals{{$p.Estimate.Approvals}}{{$p.Actual.Approvals}}
Wall time (s){{printf "%.0f" $p.Estimate.WallSeconds}}—
Network operations{{$p.Estimate.Network}}—
Money{{printf "%.2f" $p.Estimate.Money}}{{printf "%.2f" $p.Actual.Money}}
+

Completion criteria

{{$p.CompletionCriteria}}

+ {{if $p.AbortConditions}}

Abort conditions

    {{range $p.AbortConditions}}
  • {{.}}
  • {{end}}
{{end}} + {{if $p.Assumptions}}

Assumptions

    {{range $p.Assumptions}}
  • {{.Proposition}}{{if .BeliefID}} (belief, at least {{conf .MinConfidence}}){{end}}
  • {{end}}
{{end}} +

Risk

{{$p.Risk}}

+

Alternatives considered

+ {{if $p.Alternatives}}{{range $p.Alternatives}}{{end}}
StrategyExpected confidenceApprovalsScore
{{.Strategy}}
{{trunc .Description 120}}
{{conf .ExpectedConfidence}}{{.Estimate.Approvals}}{{printf "%.3f" .Score}}{{if .Selected}}selected{{end}} {{.Why}}
{{else}}

None recorded.

{{end}} +

Revisions

    {{range .Data.Revisions}}
  • revision {{.Revision}} · {{.Strategy}} · {{.Status}} {{trunc .StatusReason 100}}
  • {{end}}
+

Validation

{{.Data.Validation}}
+
+{{end}} diff --git a/web/templates/pages/plans.html b/web/templates/pages/plans.html new file mode 100644 index 0000000..e659e55 --- /dev/null +++ b/web/templates/pages/plans.html @@ -0,0 +1,12 @@ +{{define "content"}} +{{$m := .Data.Mind}} +

{{$m.DisplayName}} plans

+

Plans are revisable dependency graphs serving one intention. Go validates structure, the capability vocabulary and parameters; every action goes through policy; replans are new revisions and old plans stay as history.

+{{template "mind_tabs" (dict "M" $m "Active" "plans")}} +{{template "volition_tabs" (dict "M" $m "Active" "plans")}} +
+{{if .Data.Plans}} +{{range .Data.Plans}}{{end}}
ObjectiveRevisionStrategyOriginStatusInvocations usedApprovalsUpdated
{{.Objective}}{{with .StatusReason}}
{{trunc . 140}}
{{end}}
{{.Revision}}{{.Strategy}}{{.Origin}}{{.Status}}{{.Actual.Invocations}} / {{.Budget.MaxInvocations}}{{.Actual.Approvals}}{{ago .UpdatedAt}}
+{{else}}

No plans yet.

{{end}} +
+{{end}} diff --git a/web/templates/pages/research.html b/web/templates/pages/research.html new file mode 100644 index 0000000..477dd9d --- /dev/null +++ b/web/templates/pages/research.html @@ -0,0 +1,40 @@ +{{define "content"}} +{{$m := .Data.Mind}}{{$a := .Data.Metrics}}{{$c := .Data.Calibration}} +

{{$m.DisplayName}} cognitive research

+

{{.Data.Disclaimer}} Every figure below has an operational definition, shown beside it.

+{{template "mind_tabs" (dict "M" $m "Active" "research")}} +{{template "volition_tabs" (dict "M" $m "Active" "research")}} +
+

Volition and agency last 30 days

+ + + + + + + + + + +
Want → intention{{pct $a.WantToIntention}} of {{$a.WantsCreated}} wants
Intention → action{{pct $a.IntentionToAction}} of {{$a.IntentionsFormed}} intentions
Intention abandonment{{pct $a.IntentionAbandonment}}
Blocked intentions{{pct $a.BlockedIntentions}}
Commitment fulfilment{{pct $a.CommitmentFulfilment}} {{$a.CommitmentsFulfilled}} fulfilled, {{$a.CommitmentsBroken}} broken
Commitment conflicts{{printf "%.2f" $a.CommitmentConflictPerItem}} per commitment
Replanning{{pct $a.Replanning}} of {{$a.PlansCreated}} plans
Outcome success{{pct $a.OutcomeSuccess}}
Attribution of resolved expectations{{range $k, $v := $a.Attribution}}{{$k}} {{$v}} · {{else}}none{{end}}
+

Introspection last 30 days

+

Mean accuracy {{printf "%.2f" $a.IntrospectionAccuracy}} over {{$a.IntrospectionReplies}} replies · {{range $k, $v := .Data.IntrospectionLabels}}{{$k}} {{$v}} · {{end}}

+ {{if .Data.Introspection}}{{range .Data.Introspection}}{{end}}
{{.Label}}{{if .Value}}{{conf .Value}}{{end}}{{if .EventID}}reply{{end}}{{ago .CreatedAt}}
{{end}} +

Action claims

{{if .Data.Claims}}
    {{range .Data.Claims}}
  • {{.Label}} {{index .Detail "claim"}} {{ago .CreatedAt}}
  • {{end}}
{{else}}

No first-person action claims recorded.

{{end}} +

Self-model

{{if .Data.SelfAccuracy}}

Capability self-model accuracy {{pct (deref .Data.SelfAccuracy)}}{{if .Data.SelfErrors}}: {{join .Data.SelfErrors "; "}}{{end}}

{{else}}

The self-model lists no capabilities to compare.

{{end}} +
+
+

Metacognitive calibration last 90 days, {{$c.Samples}} cited beliefs, revision window {{$c.RevisionWindowHours}} h

+ {{if $c.Samples}}
{{range $c.Buckets}}{{printf "%.1f" .From}}–{{printf "%.1f" .To}}{{pct .RevisedRate}} revised of {{.Cited}}{{end}}
+

Brier score {{printf "%.3f" $c.Brier}} · after expressing uncertainty: {{pct $c.UncertainRevised}} revised ({{$c.UncertainSamples}}) · without: {{pct $c.ConfidentRevised}} ({{$c.ConfidentSamples}}){{if $c.ImprovedOverTime}} · first half {{printf "%.3f" $c.BrierFirstHalf}}, second half {{printf "%.3f" $c.BrierSecondHalf}} ({{if deref $c.ImprovedOverTime}}improved{{else}}not improved{{end}}){{end}}

+ {{else}}

No replies cited beliefs in the period.

{{end}} +
+

Experiments on this mind

+ {{if .Data.Experiments}} + {{range .Data.Experiments}}{{end}}
ExperimentProtocolStatusVariantsFirst divergenceArchitecture shareReproducibility
{{.E.Name}}{{.Protocol}}{{.E.Status}}{{.Variants}}{{if lt .First 0}}—{{else}}{{.First}}{{end}}{{printf "%.2f" .Share}}{{.Repro}}
+ {{else}}

None. Create one from a snapshot on the experiments page.

{{end}} +
+

Operational definitions

+ {{range $s := .Data.Sections}}

{{$s}}

{{range index $.Data.Measures $s}}
{{.Title}} {{.Name}}
{{.Definition}} Not evidence of {{.NotEvidence}}.
{{end}}
{{end}} +
+{{end}} diff --git a/web/templates/pages/research_index.html b/web/templates/pages/research_index.html new file mode 100644 index 0000000..c060100 --- /dev/null +++ b/web/templates/pages/research_index.html @@ -0,0 +1,12 @@ +{{define "content"}} +

Cognitive research

+

{{.Data.Disclaimer}}

+

Experiments fork a snapshot, feed identical stimuli to variants that differ in one respect, and measure the difference: whether entering the workspace changes downstream processing, what ablating a component does, whether the mind notices an inaccurate self-model, what would have happened without an event, and how much behaviour stays the same when the language model changes.

+

Minds

{{range .Data.Minds}}{{.DisplayName}} {{end}}

+{{range $p := .Data.Protocols}}{{with index $.Data.ByProtocol $p}}

{{title $p}}

+ +{{range .}}{{end}}
ExperimentHypothesisStatusVariantsFirst divergenceArchitecture shareReproducibility
{{.E.Name}}{{trunc .E.Hypothesis 120}}{{.E.Status}}{{.Variants}}{{if lt .First 0}}—{{else}}{{.First}}{{end}}{{printf "%.2f" .Share}}{{.Repro}}
{{end}}{{end}} +

Operational definitions

+ {{range $s := .Data.Sections}}

{{$s}}

{{range index $.Data.Measures $s}}
{{.Title}} {{.Name}}
{{.Definition}} Not evidence of {{.NotEvidence}}.
{{end}}
{{end}} +
+{{end}} diff --git a/web/templates/pages/wants.html b/web/templates/pages/wants.html new file mode 100644 index 0000000..c9c6981 --- /dev/null +++ b/web/templates/pages/wants.html @@ -0,0 +1,25 @@ +{{define "content"}} +{{$m := .Data.Mind}} +

{{$m.DisplayName}} wants

+

A want is a motivational preference with an origin and a strength that fades (half-life {{.Data.HalfLife}}). It is not a decision and never implies action; candidates below {{.Data.Threshold}} are not stored. Suppressed wants stay represented.

+{{template "mind_tabs" (dict "M" $m "Active" "wants")}} +{{template "volition_tabs" (dict "M" $m "Active" "wants")}} +
+

Active {{len .Data.Active}}

+ {{if .Data.Active}} + {{range .Data.Active}} + + + + + + {{end}}
WantOriginStrength nowUrgencyReinforcedSet asideConflicts
{{if eq .Valence "avoid"}}avoid {{end}}{{.Description}} +
{{with .Proposal}}{{if .Capability}}means: {{.Capability}} {{.Target}}{{else}}means: investigate {{.Subject}}{{end}} · {{end}}{{if .GoalID}}goal: {{index $.Data.Goals .GoalID}} · {{end}}{{if .Drives}}drives {{join .Drives ", "}} · {{end}}{{if .Values}}values {{join .Values ", "}} · {{end}}{{if .OriginEvent}}origin event{{end}}
{{.Origin}} {{conf .Eff}}
stored {{conf .Strength}}
{{conf .Urgency}}{{.Reinforcements}}{{if .Suppressions}}{{.Suppressions}}{{else}}0{{end}}{{range .Conflicts}}
{{if .Preferred}}preferred{{else}}other preferred{{end}} vs “{{trunc .Other 60}}” ({{.Kind}}, {{.Outcome}})
{{.Rule}}
{{else}}none{{end}}
+ {{else}}

No active wants. Wants come from failed predictions, contradictions, operator goals, stalled goals, questions and requests, and due commitments.

{{end}} +
+
+

Closed {{len .Data.Closed}}

+ {{if .Data.Closed}} + {{range .Data.Closed}}{{end}}
WantStatusResolutionWhen
{{.Description}}{{.Status}}{{.Resolution}}{{if .ResolvedAt}}{{ago .ResolvedAt}}{{end}}
{{else}}

None.

{{end}} +
+{{end}} diff --git a/web/templates/pages/why.html b/web/templates/pages/why.html new file mode 100644 index 0000000..5c2d554 --- /dev/null +++ b/web/templates/pages/why.html @@ -0,0 +1,23 @@ +{{define "content"}} +{{$m := .Data.Mind}}{{$x := .Data.X}} +

agency

Why did this happen?

+

Constructed only from recorded state: rows and events. Nothing here is generated, and hidden model reasoning is not shown because it is not stored. Anything missing is listed as not recorded.

+{{template "mind_tabs" (dict "M" $m "Active" "agency")}} +
+{{with $x.Action}}

Action

{{.Capability}} on {{.Target}} — {{.Status}}{{if .DecidedBy}} · decided by {{.DecidedBy}}{{end}}{{with .Note}} (“{{.}}”){{end}}{{with .Result}}
{{.}}{{end}}

{{end}} +{{with $x.Step}}

Immediate cause

plan step {{.Key}} {{.Title}} ({{.Status}}){{with $x.Plan}} of {{.Text}} {{.Detail}}{{end}}

{{end}} +{{with $x.Intention}}

Intention

{{.Description}} {{.Status}} {{.Kind}}, priority {{conf .Priority}}

{{end}} +{{with $x.Subordinate}}

While waiting

{{.Description}} {{.Status}}

{{end}} +{{if $x.Wants}}

Originating wants

{{range $x.Wants}}

{{.Description}} ({{.Origin}}, strength {{conf .Strength}})

{{end}}
{{end}} +{{with $x.Goal}}

Goal

{{.Text}} {{.Detail}}

{{end}} +{{if $x.Beliefs}}

Supporting beliefs

{{range $x.Beliefs}}

{{.Text}} {{.Detail}}

{{end}}
{{end}} +{{if $x.Evidence}}

Evidence

    {{range $x.Evidence}}
  • {{.Text}} {{.Detail}}
  • {{end}}
{{end}} +{{if $x.ConflictingWants}}

Conflicting wants

{{range $x.ConflictingWants}}

{{.Text}}
{{.Detail}}

{{end}}
{{end}} +{{if $x.Commitments}}

Commitments

{{range $x.Commitments}}

{{.Polarity}}: {{.Description}}{{if .Unless}} unless {{.Unless}}{{end}} ({{.Kind}})

{{end}}
{{end}} +{{if $x.Resolution}}

Resolution

    {{range $x.Resolution}}
  • {{.}}
  • {{end}}
{{end}} +{{with $x.Expectation}}

Expected outcome

{{.Text}} — {{.Detail}}

{{end}} +{{with .Data.Attribution}}

Attribution

{{.Class}} {{.Reason}}

{{end}} +{{if $x.Confidence}}

Confidence

{{conf $x.Confidence}} the intention's expected chance of achieving its outcome, as recorded when it formed

{{end}} +{{if $x.Gaps}}

Not recorded

    {{range $x.Gaps}}
  • {{.}}
  • {{end}}
{{end}} +
+{{end}} diff --git a/web/templates/partials/mind_tabs.html b/web/templates/partials/mind_tabs.html index a7f56bb..52b9323 100644 --- a/web/templates/partials/mind_tabs.html +++ b/web/templates/partials/mind_tabs.html @@ -3,6 +3,6 @@ Overview Thoughts Personality - WorkspaceMemoryBeliefsConversationDreamsSnapshotsReplayRedactions + WorkspaceMemoryBeliefsConversationVolitionDreamsSnapshotsReplayRedactions {{end}} diff --git a/web/templates/partials/plan_dag.html b/web/templates/partials/plan_dag.html new file mode 100644 index 0000000..a3b2ec4 --- /dev/null +++ b/web/templates/partials/plan_dag.html @@ -0,0 +1,13 @@ +{{define "plan_dag"}}{{/* .Levels []dagLevel, .M mind */}} +
+{{range .Levels}}

{{if eq .Level 0}}first{{else}}after {{.Level}} step(s){{end}}

+ {{range .Steps}}
+ {{.Key}} {{.Status}}
{{.Title}} +
{{.Kind}}{{if .Capability}} · {{.Capability}}{{end}}{{if .DependsOn}} · after {{join .DependsOn ", "}}{{end}}
+ {{with .Expectation}}
expects: {{.Description}}
{{end}} + {{with .StatusReason}}
{{trunc . 140}}
{{end}} + {{with .Error}}
{{trunc . 140}}
{{end}} +
{{end}} +
{{end}} +
+{{end}} diff --git a/web/templates/partials/volition_tabs.html b/web/templates/partials/volition_tabs.html new file mode 100644 index 0000000..4f04001 --- /dev/null +++ b/web/templates/partials/volition_tabs.html @@ -0,0 +1,10 @@ +{{define "volition_tabs"}}{{$m := .M}}{{$a := .Active}} + +{{end}}