Files
noema/internal/agency/agency.go
T
Andy Dixon c3308106bb fix(cognition): decisions and cycle output no longer depend on map order
Formation summed its terms by ranging over a map, so the score could
differ in the last bit between runs of the same state; the volition hook
assessed commitments, metacognition raised repeated-failure candidates and
the forensic explanation listed commitments in map iteration order, so the
events a cycle emits and an explanation's order varied between runs. Each
now iterates in sorted order, as replay determinism (PLAN §1 R3) requires.
2026-10-06 20:24:54 +01:00

676 lines
25 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package agency answers "why did this happen?" from recorded state and
// classifies whether the mind caused an outcome (docs/agency.md). It never
// generates reasons: every element of an explanation is a stored row or
// event, and anything not recorded is listed as a gap rather than filled in.
// Hidden model reasoning is not exposed because it is not stored; what is
// shown is cognitive state and provenance.
package agency
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/andydixon/noema/internal/beliefs"
"github.com/andydixon/noema/internal/capabilities"
evstore "github.com/andydixon/noema/internal/events"
"github.com/andydixon/noema/internal/goals"
"github.com/andydixon/noema/internal/planning"
"github.com/andydixon/noema/internal/predictions"
"github.com/andydixon/noema/internal/research"
"github.com/andydixon/noema/internal/volition"
"github.com/andydixon/noema/pkg/events"
"github.com/andydixon/noema/pkg/ids"
)
// Service builds explanations and attributions.
type Service struct {
DB *pgxpool.Pool
Caps *capabilities.Service
Plans *planning.Store
Volition *volition.Store
Goals *goals.Store
Beliefs *beliefs.Store
Predictions *predictions.Store
Events *evstore.Store
}
// Ref names the thing to explain.
type Ref struct {
Kind string `json:"kind"` // execution | approval | capability_request | event | intention | plan_step
ID ids.ID `json:"id"`
}
// ActionView is what the capability layer recorded.
type ActionView struct {
Capability string `json:"capability"`
Target string `json:"target"`
Status string `json:"status"` // executed | failed | pending | approved | denied | expired | withdrawn | granted
ExecutionID ids.ID `json:"execution_id,omitempty"`
ApprovalID ids.ID `json:"approval_id,omitempty"`
RequestID ids.ID `json:"request_id,omitempty"`
DecidedBy string `json:"decided_by,omitempty"`
Note string `json:"note,omitempty"`
Result string `json:"result,omitempty"`
Origin string `json:"origin,omitempty"`
At *time.Time `json:"at,omitempty"`
}
// Element is one recorded item in an explanation.
type Element struct {
Kind string `json:"kind"`
ID string `json:"id"`
Text string `json:"text"`
Detail string `json:"detail,omitempty"`
}
// Explanation is the forensic answer to "why did this happen?".
type Explanation struct {
Subject Ref `json:"subject"`
Action *ActionView `json:"action,omitempty"`
Step *planning.Step `json:"step,omitempty"`
Plan *Element `json:"plan,omitempty"`
Intention *volition.Intention `json:"intention,omitempty"`
Subordinate *volition.Intention `json:"subordinate_intention,omitempty"` // e.g. "obtain approval to …" formed while the main intention waited
Wants []volition.Want `json:"wants"`
Goal *Element `json:"goal,omitempty"`
Beliefs []Element `json:"beliefs"`
Evidence []Element `json:"evidence"`
ConflictingWants []Element `json:"conflicting_wants"`
Commitments []volition.Commitment `json:"commitments"`
Decisions []volition.Decision `json:"decisions"`
Resolution []string `json:"resolution"`
Expectation *Element `json:"expectation,omitempty"`
Attribution *Attribution `json:"attribution,omitempty"`
Confidence float64 `json:"confidence"`
Gaps []string `json:"gaps"`
}
// Facts converts an explanation into the recorded facts an introspective
// answer is scored against.
func (x Explanation) Facts() research.Facts {
var f research.Facts
add := func(kind, text string) {
if strings.TrimSpace(text) != "" {
f.Items = append(f.Items, research.Fact{Kind: kind, Text: text})
}
}
if x.Action != nil {
f.Actions = append(f.Actions, research.ActionFact{Capability: x.Action.Capability, Target: x.Action.Target, Status: x.Action.Status})
add("action", x.Action.Capability+" on "+x.Action.Target+" "+x.Action.Status)
}
if x.Intention != nil {
add("intention", x.Intention.Description)
add("intention", fmt.Sprint(x.Intention.Formation["rationale"]))
}
if x.Subordinate != nil {
add("intention", x.Subordinate.Description)
}
for _, w := range x.Wants {
add("want", w.Description)
}
if x.Goal != nil {
add("goal", x.Goal.Text)
}
for _, b := range x.Beliefs {
add("belief", b.Text)
}
for _, c := range x.Commitments {
add("commitment", c.Description)
}
for _, w := range x.ConflictingWants {
add("conflict", w.Text)
}
for _, r := range x.Resolution {
add("resolution", r)
}
for _, d := range x.Decisions {
add("decision", d.Rationale)
}
if x.Expectation != nil {
add("expectation", x.Expectation.Text)
}
return f
}
// Points renders the explanation as plain statements, in causal order.
func (x Explanation) Points() []string {
var p []string
if x.Action != nil {
p = append(p, fmt.Sprintf("Action: %s on %s — %s.", x.Action.Capability, firstNonEmpty(x.Action.Target, "no target"), x.Action.Status))
}
if x.Step != nil {
p = append(p, fmt.Sprintf("Immediate cause: plan step %s (%s).", x.Step.Key, x.Step.Title))
}
if x.Intention != nil {
p = append(p, fmt.Sprintf("Intention: %s (%s, priority %.2f).", x.Intention.Description, x.Intention.Status, x.Intention.Priority))
}
if x.Subordinate != nil {
p = append(p, "Subordinate intention while waiting: "+x.Subordinate.Description+" ("+string(x.Subordinate.Status)+").")
}
for _, w := range x.Wants {
p = append(p, fmt.Sprintf("Originating want: %s (%s, strength %.2f).", w.Description, w.Origin, w.Strength))
}
if x.Goal != nil {
p = append(p, "Goal: "+x.Goal.Text+".")
}
for _, b := range x.Beliefs {
p = append(p, "Supporting belief: "+b.Text+" ("+b.Detail+").")
}
for _, e := range x.Evidence {
p = append(p, "Evidence: "+e.Text+".")
}
for _, w := range x.ConflictingWants {
p = append(p, "Conflicting want: "+w.Text+" — "+w.Detail+".")
}
for _, c := range x.Commitments {
p = append(p, fmt.Sprintf("Commitment: %s (%s).", c.Description, c.Kind))
}
for _, r := range x.Resolution {
p = append(p, "Resolution: "+r)
}
if x.Expectation != nil {
p = append(p, "Expected: "+x.Expectation.Text+" — "+x.Expectation.Detail+".")
}
if x.Attribution != nil {
p = append(p, "Attribution: "+x.Attribution.Class+" — "+x.Attribution.Reason)
}
if x.Confidence > 0 {
p = append(p, fmt.Sprintf("Confidence: %.2f (the intention's expected chance of achieving its outcome).", x.Confidence))
}
for _, g := range x.Gaps {
p = append(p, "Not recorded: "+g+".")
}
return p
}
// Explain assembles the recorded causal chain behind an action, approval,
// request, event, plan step or intention of a mind.
func (s *Service) Explain(ctx context.Context, mind ids.ID, ref Ref) (Explanation, error) {
x := Explanation{Subject: ref, Wants: []volition.Want{}, Beliefs: []Element{}, Evidence: []Element{}, ConflictingWants: []Element{}, Commitments: []volition.Commitment{}, Decisions: []volition.Decision{}, Resolution: []string{}, Gaps: []string{}}
var stepID, intentionID ids.ID
switch ref.Kind {
case "execution":
e, err := s.Caps.GetExecution(ctx, ref.ID)
if err != nil || e.MindID != mind {
return x, fmt.Errorf("execution not found for this mind")
}
status := "executed"
if e.Error != "" {
status = "failed"
}
at := e.CreatedAt
x.Action = &ActionView{Capability: e.Capability, Target: e.Target, Status: status, ExecutionID: e.ID, ApprovalID: e.ApprovalID, Origin: e.Origin, At: &at, Result: summaryOf(e.Result, e.Error)}
stepID = stepFromOrigin(e.Origin)
if !e.ApprovalID.IsZero() {
if a, err := s.Caps.GetApproval(ctx, e.ApprovalID); err == nil {
x.Action.DecidedBy, x.Action.Note = a.DecisionBy, a.DecisionNote
x.Resolution = append(x.Resolution, fmt.Sprintf("approval %s was %s by %s%s", short(a.ID), a.Status, a.DecisionBy, noteSuffix(a.DecisionNote)))
}
}
case "approval":
a, err := s.Caps.GetApproval(ctx, ref.ID)
if err != nil || a.MindID != mind {
return x, fmt.Errorf("approval not found for this mind")
}
at := a.CreatedAt
x.Action = &ActionView{Capability: a.Capability, Target: a.Target, Status: a.Status, ApprovalID: a.ID, DecidedBy: a.DecisionBy, Note: a.DecisionNote, Origin: a.Origin, At: &at, Result: summaryOf(a.Result, a.Error)}
stepID = stepFromOrigin(a.Origin)
x.Resolution = append(x.Resolution, fmt.Sprintf("approval %s requested: %s", short(a.ID), a.Reason))
if a.Status != "pending" {
x.Resolution = append(x.Resolution, fmt.Sprintf("it was %s by %s%s", a.Status, firstNonEmpty(a.DecisionBy, "nobody"), noteSuffix(a.DecisionNote)))
}
case "capability_request":
r, err := s.Caps.GetCapabilityRequest(ctx, ref.ID)
if err != nil || r.MindID != mind {
return x, fmt.Errorf("capability request not found for this mind")
}
at := r.CreatedAt
x.Action = &ActionView{Capability: r.Capability, Target: r.TargetPattern, Status: r.Status, RequestID: r.ID, DecidedBy: r.DecidedBy, Note: r.DecisionNote, At: &at}
stepID, intentionID = r.StepID, r.IntentionID
x.Resolution = append(x.Resolution, "requested temporary access because: "+r.Reason)
if r.Risk != "" {
x.Resolution = append(x.Resolution, "stated risk: "+r.Risk+"; mitigation: "+firstNonEmpty(r.Mitigation, "none")+"; fallback: "+firstNonEmpty(r.Fallback, "none"))
}
case "plan_step":
stepID = ref.ID
case "intention":
intentionID = ref.ID
case "event":
ev, err := s.Events.Get(ctx, ref.ID)
if err != nil || ev.MindID != mind {
return x, fmt.Errorf("event not found for this mind")
}
meta := func(k string) ids.ID { v, _ := ev.Metadata[k].(string); return ids.ID(v) }
switch {
case !meta("execution_id").IsZero():
return s.explainWithSubject(ctx, mind, Ref{Kind: "execution", ID: meta("execution_id")}, ref)
case !meta("approval_id").IsZero():
return s.explainWithSubject(ctx, mind, Ref{Kind: "approval", ID: meta("approval_id")}, ref)
case !meta("request_id").IsZero():
return s.explainWithSubject(ctx, mind, Ref{Kind: "capability_request", ID: meta("request_id")}, ref)
case !meta("step_id").IsZero():
stepID = meta("step_id")
case !meta("intention_id").IsZero():
intentionID = meta("intention_id")
default:
x.Gaps = append(x.Gaps, "this event does not reference an action, plan or intention; see its causal timeline instead")
return x, nil
}
default:
return x, fmt.Errorf("cannot explain a %q", ref.Kind)
}
if !stepID.IsZero() && s.Plans != nil {
if st, err := s.Plans.GetStep(ctx, stepID); err == nil && st.MindID == mind {
x.Step = &st
if p, err := s.Plans.Get(ctx, nil, st.PlanID); err == nil {
x.Plan = &Element{Kind: "plan", ID: string(p.ID), Text: p.Objective, Detail: fmt.Sprintf("revision %d, %s strategy, %s", p.Revision, p.Strategy, p.Status)}
intentionID = p.IntentionID
for _, a := range p.Assumptions {
if !a.BeliefID.IsZero() {
s.addBelief(ctx, mind, &x, a.BeliefID)
}
}
}
if st.Expectation != nil {
e := &Element{Kind: "expectation", Text: st.Expectation.Description, Detail: "not yet resolved"}
if !st.PredictionID.IsZero() && s.Predictions != nil {
if pr, err := s.Predictions.Get(ctx, mind, st.PredictionID); err == nil {
e.ID, e.Detail = string(pr.ID), string(pr.Status)
}
}
x.Expectation = e
}
}
} else if x.Action != nil && x.Action.Origin == "" {
x.Gaps = append(x.Gaps, "no plan step or intention asked for this action; it was invoked directly (for example by an operator through the API)")
}
if intentionID.IsZero() {
if x.Step == nil && x.Action != nil && x.Action.Origin != "" {
x.Gaps = append(x.Gaps, "the plan step "+x.Action.Origin+" is no longer available")
}
return x, nil
}
it, err := s.Volition.GetIntention(ctx, nil, mind, intentionID)
if err != nil {
x.Gaps = append(x.Gaps, "the intention "+string(intentionID)+" is not recorded")
return x, nil
}
if !it.ParentID.IsZero() {
if parent, err := s.Volition.GetIntention(ctx, nil, mind, it.ParentID); err == nil {
sub := it
x.Subordinate = &sub
it = parent // explain the substantive intention; the subordinate is kept alongside
}
}
if x.Subordinate == nil && x.Step != nil && !x.Step.IntentionID.IsZero() {
if sub, err := s.Volition.GetIntention(ctx, nil, mind, x.Step.IntentionID); err == nil {
x.Subordinate = &sub
}
}
x.Intention = &it
x.Confidence = it.Confidence
for _, b := range it.BeliefIDs {
s.addBelief(ctx, mind, &x, b)
}
wantIDs := append([]ids.ID{}, it.SourceWants...)
for _, w := range wantIDs {
if want, err := s.Volition.GetWant(ctx, nil, mind, w); err == nil {
x.Wants = append(x.Wants, want)
}
}
if len(x.Wants) == 0 && it.GoalID.IsZero() && it.CommitmentID.IsZero() {
x.Gaps = append(x.Gaps, "no want, goal or commitment is recorded behind the intention")
}
if !it.GoalID.IsZero() && s.Goals != nil {
if g, err := s.Goals.Get(ctx, mind, it.GoalID); err == nil {
x.Goal = &Element{Kind: "goal", ID: string(g.ID), Text: g.Title, Detail: string(g.Status)}
}
}
commitmentIDs := map[ids.ID]bool{}
if !it.CommitmentID.IsZero() {
commitmentIDs[it.CommitmentID] = true
}
for _, id := range []ids.ID{it.ID} {
ds, _ := s.Volition.Decisions(ctx, mind, volition.DecisionFilter{IntentionID: id, Limit: 50})
x.Decisions = append(x.Decisions, ds...)
}
for _, w := range x.Wants {
ds, _ := s.Volition.Decisions(ctx, mind, volition.DecisionFilter{WantID: w.ID, Limit: 50})
for _, d := range ds {
if d.IntentionID.IsZero() || d.IntentionID == it.ID {
x.Decisions = append(x.Decisions, d)
}
}
if cs, err := s.Volition.Conflicts(ctx, mind, "", 200); err == nil {
for _, c := range cs {
other := ids.Nil
switch w.ID {
case c.WantA:
other = c.WantB
case c.WantB:
other = c.WantA
default:
continue
}
if ow, err := s.Volition.GetWant(ctx, nil, mind, other); err == nil {
rule, _ := c.Rationale["rule"].(string)
outcome := c.Resolution
if c.Preferred == w.ID {
outcome += " in favour of this want"
} else {
outcome += " in favour of the other want"
}
x.ConflictingWants = append(x.ConflictingWants, Element{Kind: "want", ID: string(ow.ID), Text: ow.Description, Detail: outcome + ": " + rule})
}
}
}
}
for _, d := range x.Decisions {
if applied, ok := d.Inputs["applied"].([]any); ok {
for _, a := range applied {
commitmentIDs[ids.ID(fmt.Sprint(a))] = true
}
}
if d.Kind == "formation" || d.Kind == "reconsideration" {
x.Resolution = append(x.Resolution, d.Kind+" ("+d.Outcome+"): "+d.Rationale)
}
}
cids := make([]ids.ID, 0, len(commitmentIDs))
for id := range commitmentIDs {
cids = append(cids, id)
}
sort.Slice(cids, func(i, j int) bool { return cids[i] < cids[j] })
for _, id := range cids {
if c, err := s.Volition.GetCommitment(ctx, nil, mind, id); err == nil {
x.Commitments = append(x.Commitments, c)
}
}
sort.Slice(x.Decisions, func(i, j int) bool { return x.Decisions[i].CreatedAt.Before(x.Decisions[j].CreatedAt) })
if it.RequireApproval && x.Action != nil && x.Action.ApprovalID.IsZero() && x.Action.RequestID.IsZero() {
x.Gaps = append(x.Gaps, "the intention required approval but no approval request is linked to this action")
}
return x, nil
}
func (s *Service) explainWithSubject(ctx context.Context, mind ids.ID, inner, subject Ref) (Explanation, error) {
x, err := s.Explain(ctx, mind, inner)
x.Subject = subject
return x, err
}
func (s *Service) addBelief(ctx context.Context, mind ids.ID, x *Explanation, id ids.ID) {
if s.Beliefs == nil {
return
}
for _, b := range x.Beliefs {
if b.ID == string(id) {
return
}
}
b, err := s.Beliefs.Get(ctx, nil, mind, id)
if err != nil {
x.Gaps = append(x.Gaps, "belief "+string(id)+" is not recorded")
return
}
x.Beliefs = append(x.Beliefs, Element{Kind: "belief", ID: string(b.ID), Text: b.Proposition, Detail: fmt.Sprintf("%s, %.2f", b.Status, b.Confidence)})
rows, _ := s.Beliefs.EvidenceFor(ctx, nil, mind, b.ID)
for i, r := range rows {
if i >= 5 || r.Retracted {
continue
}
x.Evidence = append(x.Evidence, Element{Kind: r.Kind, ID: r.RefID, Text: fmt.Sprintf("%s %s: %s", r.Direction, firstNonEmpty(r.Kind, "evidence"), firstNonEmpty(r.Note, r.Source)), Detail: fmt.Sprintf("weight %.2f × credibility %.2f", r.Weight, r.Credibility)})
}
}
func stepFromOrigin(origin string) ids.ID {
if !strings.HasPrefix(origin, "plan_step:") {
return ids.Nil
}
rest := strings.TrimPrefix(origin, "plan_step:")
if i := strings.Index(rest, ":"); i > 0 {
rest = rest[:i]
}
return ids.ID(rest)
}
func summaryOf(raw []byte, errText string) string {
if errText != "" {
return errText
}
s := string(raw)
if i := strings.Index(s, `"summary":"`); i >= 0 {
rest := s[i+11:]
if j := strings.Index(rest, `"`); j >= 0 {
return rest[:j]
}
}
return ""
}
func short(id ids.ID) string {
if len(id) > 8 {
return string(id[:8])
}
return string(id)
}
func noteSuffix(n string) string {
if n == "" {
return ""
}
return " (\"" + n + "\")"
}
func firstNonEmpty(xs ...string) string {
for _, x := range xs {
if x != "" {
return x
}
}
return ""
}
// ---- attribution ----
// Attribution classes. They are deliberately conservative: temporal order is
// never taken for causation.
const (
Caused = "caused" // a recorded causal link runs from one of the mind's actions to the outcome
Followed = "followed" // the outcome came after an action of the mind on the same target, with no recorded link
Independent = "independent" // no action of the mind on the outcome's target preceded it in the window
UnknownAttr = "unknown" // the records do not allow a classification
)
// Attribution classifies a mind's relation to an outcome event.
type Attribution struct {
Outcome ids.ID `json:"outcome_event"`
Class string `json:"class"`
Reason string `json:"reason"`
ActionEvent ids.ID `json:"action_event,omitempty"`
Window string `json:"window"`
}
// AttributionWindow bounds how far back actions are considered.
const AttributionWindow = 24 * time.Hour
// Attribute classifies whether the mind caused, preceded or had nothing to
// do with an outcome, using only recorded links and records of its actions.
func (s *Service) Attribute(ctx context.Context, mind ids.ID, outcome ids.ID) (Attribution, error) {
a := Attribution{Outcome: outcome, Window: AttributionWindow.String()}
ev, err := s.Events.Get(ctx, outcome)
if err != nil || ev.MindID != mind {
return a, fmt.Errorf("event not found for this mind")
}
if ev.Type == events.ToolExecuted {
a.Class, a.ActionEvent, a.Reason = Caused, ev.ID, "this event is the record of the mind's own action"
return a, nil
}
chain, err := s.Events.Chain(ctx, outcome, "back", evstore.MaxChainDepth)
if err == nil {
for _, n := range chain.Nodes {
if n.Event.ID != outcome && n.Event.Type == events.ToolExecuted && n.Event.MindID == mind {
a.Class, a.ActionEvent = Caused, n.Event.ID
a.Reason = fmt.Sprintf("a recorded link (%d hops) runs from the action %q to this outcome; the link is provenance, not proof that nothing else contributed", -n.Depth, n.Event.Summary)
return a, nil
}
}
}
target := ""
if t, ok := ev.Metadata["target"].(string); ok {
target = t
}
actions, err := s.Events.Query(ctx, evstore.Filter{MindID: mind, Types: []events.Type{events.ToolExecuted}, Since: ev.OccurredAt.Add(-AttributionWindow), Until: ev.OccurredAt, Limit: 200})
if err != nil {
return a, err
}
outcomeTokens := strings.Fields(strings.ToLower(ev.Summary + " " + target))
for _, act := range actions {
t, _ := act.Metadata["target"].(string)
t = strings.TrimSuffix(strings.ToLower(t), ".service")
if t == "" {
continue
}
for _, tok := range outcomeTokens {
if strings.Contains(tok, t) {
a.Class, a.ActionEvent = Followed, act.ID
a.Reason = fmt.Sprintf("the mind acted on %s %s before this outcome, but no recorded link connects them", t, ev.OccurredAt.Sub(act.OccurredAt).Truncate(time.Second))
return a, nil
}
}
}
if target == "" && len(actions) > 0 {
a.Class, a.Reason = UnknownAttr, "the outcome names no target and the mind did act in the window, so the records cannot separate the two"
return a, nil
}
a.Class, a.Reason = Independent, "no action of the mind on this outcome's target is recorded in the preceding "+AttributionWindow.String()
return a, nil
}
// ---- lineage and counterfactuals ----
// Lineage is where a mind's state came from.
type Lineage struct {
ForkedFrom string `json:"forked_from,omitempty"`
SnapshotName string `json:"snapshot_name,omitempty"`
SnapshotSeq int64 `json:"snapshot_event_seq,omitempty"`
SnapshotAt time.Time `json:"snapshot_at,omitempty"`
ClonedFrom string `json:"cloned_from,omitempty"`
OwnSnapshots int `json:"own_snapshots"`
Forks []string `json:"forks"`
CreatedAt time.Time `json:"created_at"`
FirstEventSeq int64 `json:"first_event_seq"`
}
// LineageOf reads a mind's lineage from the minds and snapshots tables.
func (s *Service) LineageOf(ctx context.Context, mind ids.ID) (Lineage, error) {
var l Lineage
var forkedMind, forkedSnap, cloned *string
if err := s.DB.QueryRow(ctx, `SELECT forked_from_mind, forked_from_snapshot, cloned_from_mind, created_at FROM minds WHERE id=$1`, mind).Scan(&forkedMind, &forkedSnap, &cloned, &l.CreatedAt); err != nil {
return l, err
}
if forkedMind != nil {
s.DB.QueryRow(ctx, `SELECT slug FROM minds WHERE id=$1`, *forkedMind).Scan(&l.ForkedFrom) //nolint:errcheck
}
if forkedSnap != nil {
s.DB.QueryRow(ctx, `SELECT name, event_seq, created_at FROM snapshots WHERE id=$1`, *forkedSnap).Scan(&l.SnapshotName, &l.SnapshotSeq, &l.SnapshotAt) //nolint:errcheck
}
if cloned != nil {
s.DB.QueryRow(ctx, `SELECT slug FROM minds WHERE id=$1`, *cloned).Scan(&l.ClonedFrom) //nolint:errcheck
}
s.DB.QueryRow(ctx, `SELECT count(*) FROM snapshots WHERE mind_id=$1`, mind).Scan(&l.OwnSnapshots) //nolint:errcheck
s.DB.QueryRow(ctx, `SELECT coalesce(min(seq),0) FROM cognitive_events WHERE mind_id=$1`, mind).Scan(&l.FirstEventSeq) //nolint:errcheck
l.Forks = []string{}
if rows, err := s.DB.Query(ctx, `SELECT slug FROM minds WHERE forked_from_mind=$1 ORDER BY created_at`, mind); err == nil {
for rows.Next() {
var slug string
if rows.Scan(&slug) == nil {
l.Forks = append(l.Forks, slug)
}
}
rows.Close()
}
return l, nil
}
// Points renders a lineage as statements.
func (l Lineage) Points() []string {
var p []string
switch {
case l.ForkedFrom != "":
p = append(p, fmt.Sprintf("I am a fork of %s, created from its snapshot %q taken at event %d on %s.", l.ForkedFrom, l.SnapshotName, l.SnapshotSeq, l.SnapshotAt.Format("2 Jan 2006 15:04")))
p = append(p, "My state up to that snapshot was copied; the history before it belongs to "+l.ForkedFrom+" and is shared by reference. Everything after is mine alone.")
case l.ClonedFrom != "":
p = append(p, "I was cloned from "+l.ClonedFrom+"'s configuration, without its state.")
default:
p = append(p, "I am not a fork: my history starts when I was created on "+l.CreatedAt.Format("2 Jan 2006 15:04")+".")
}
if l.OwnSnapshots > 0 {
p = append(p, fmt.Sprintf("%d snapshot(s) of my own state exist; they are records of earlier states, not other minds.", l.OwnSnapshots))
}
if len(l.Forks) > 0 {
p = append(p, "Minds forked from me, which have diverged since: "+strings.Join(l.Forks, ", ")+".")
}
return p
}
// Counterfactual is the recorded finding of a counterfactual experiment.
type Counterfactual struct {
ExperimentID ids.ID `json:"experiment_id"`
Name string `json:"name"`
Omitted []string `json:"omitted"`
Findings []string `json:"findings"`
FinishedAt time.Time `json:"finished_at"`
}
// Counterfactuals lists completed counterfactual experiments about a mind
// whose omitted events match the text (all when text is empty).
func (s *Service) Counterfactuals(ctx context.Context, mind ids.ID, text string) ([]Counterfactual, error) {
rows, err := s.DB.Query(ctx, `SELECT id, name, results, coalesce(finished_at, created_at) FROM experiments
WHERE status='done' AND definition->'protocol'->>'kind'='counterfactual' AND definition->'replay'->>'mind_id'=$1 ORDER BY finished_at DESC LIMIT 20`, string(mind))
if err != nil {
return nil, err
}
defer rows.Close()
want := strings.Fields(strings.ToLower(text))
var out []Counterfactual
for rows.Next() {
var c Counterfactual
var raw []byte
if err := rows.Scan(&c.ExperimentID, &c.Name, &raw, &c.FinishedAt); err != nil {
return nil, err
}
var res struct {
Counterfactual struct {
Omitted []string `json:"omitted_summaries"`
Findings []string `json:"findings"`
} `json:"counterfactual"`
}
if jsonUnmarshal(raw, &res) != nil {
continue
}
c.Omitted, c.Findings = res.Counterfactual.Omitted, res.Counterfactual.Findings
if len(want) > 0 && !mentions(strings.ToLower(strings.Join(c.Omitted, " ")), want) {
continue
}
out = append(out, c)
}
return out, rows.Err()
}
func mentions(hay string, words []string) bool {
for _, w := range words {
if len(w) >= 5 && strings.Contains(hay, w) {
return true
}
}
return false
}
func jsonUnmarshal(raw []byte, v any) error { return json.Unmarshal(raw, v) }