Files
Andy Dixon 4a7dafc71f feat(agency): forensic "why", attribution and introspection accuracy
- agency.Explain answers "why did this happen?" for an execution,
  approval, capability request, plan step, intention or event from
  recorded state only: action, immediate plan step, plan, intention and
  any subordinate intention, originating wants, goal, supporting beliefs
  and their evidence, conflicting wants with their resolution,
  commitments applied, formation and reconsideration decisions, the
  expectation and its outcome, and explicit gaps where nothing is
  recorded. Hidden model reasoning is never shown because it is not
  stored.
- agency.Attribute classifies an outcome as caused (a recorded causal
  link from one of the mind's actions), followed (the mind acted on the
  same target earlier with no recorded link), independent or unknown;
  temporal order alone is never taken for causation.
- New introspection intents answer from records: why an action happened,
  what was believed at a past time (from append-only belief versions),
  which earlier state is the mind's (lineage), what would have happened
  without an event (only from completed counterfactual experiments,
  otherwise "no evidence"), how the mind changed, and what it wants,
  intends and has committed to.
- research.ScoreIntrospection compares a reply with the recorded facts
  sentence by sentence (supported, partially supported, unsupported,
  contradicted, unknown). Fluent reasons with no recorded counterpart earn
  nothing, and a claim that an action was done is checked against
  execution records. Every reply is scored; results are append-only
  research measurements (migration 0028), and failures raise
  unsupported_explanation or action_claim_without_record warnings.
2026-09-16 23:53:06 +01:00

806 lines
36 KiB
Go
Raw Permalink 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 cognition
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/andydixon/noema/internal/agency"
"github.com/andydixon/noema/internal/beliefs"
"github.com/andydixon/noema/internal/brain"
"github.com/andydixon/noema/internal/conversation"
"github.com/andydixon/noema/internal/epistemics"
evstore "github.com/andydixon/noema/internal/events"
"github.com/andydixon/noema/internal/llm"
"github.com/andydixon/noema/internal/memory"
"github.com/andydixon/noema/internal/research"
"github.com/andydixon/noema/internal/volition"
"github.com/andydixon/noema/internal/workspace"
"github.com/andydixon/noema/pkg/concepts"
"github.com/andydixon/noema/pkg/events"
"github.com/andydixon/noema/pkg/ids"
)
// Intent is the structured decision of what to say, built from state before
// any language is generated. It is stored with the reply so the operator can
// check that the words match the intent and the intent matches the evidence.
type Intent struct {
Kind string `json:"kind"` // answer | introspect | acknowledge | clarify | decline | greet | unknown
Summary string `json:"summary"`
Points []string `json:"points"`
Beliefs []BeliefRef `json:"beliefs"`
Memories []MemRef `json:"memories"`
Uncertainty string `json:"uncertainty,omitempty"`
Limitations []string `json:"limitations,omitempty"`
Analysis any `json:"analysis"`
// Facts are the recorded elements an introspective answer is scored
// against (docs/cognitive-research.md, introspection accuracy).
Facts *research.Facts `json:"facts,omitempty"`
}
// BeliefRef cites a belief.
type BeliefRef struct {
ID ids.ID `json:"id"`
Proposition string `json:"proposition"`
Confidence float64 `json:"confidence"`
Status string `json:"status"`
}
// MemRef cites a memory.
type MemRef struct {
ID ids.ID `json:"id"`
Kind string `json:"kind"`
Content string `json:"content"`
Confidence float64 `json:"confidence"`
}
// perceive is the handler for admitted "perceive" thoughts: it records the
// perception, encodes the episode, turns claims into beliefs, builds the
// response intent and generates the reply.
func (h Handlers) perceive(ctx context.Context, c *brain.Cycle, t workspace.Thought) error {
raw, _ := json.Marshal(t.Content["analysis"])
var a conversation.Analysis
json.Unmarshal(raw, &a) //nolint:errcheck
text, _ := t.Content["text"].(string)
actor, _ := t.Content["actor"].(string)
convID, _ := t.Content["conversation_id"].(string)
msgID, _ := t.Content["message_id"].(string)
srcID, _ := t.Content["source_event"].(string)
trust := events.Trust(fmt.Sprint(t.Content["trust"]))
if !trust.Valid() {
trust = events.TrustUser
}
mind := c.State.Mind
if call, ok := t.Content["llm_call"].(string); ok && call != "" {
c.Emit(events.New{Type: events.LLMResponse, Source: "brain.perceive", Summary: "perception enriched by " + fmt.Sprint(t.Content["llm_model"]), Salience: 0.1, Confidence: 0.8,
CausationID: t.EventID, Evidence: []events.EvidenceRef{{Kind: "llm_call", ID: call}}, Metadata: map[string]any{"role": "perceive", "call_id": call}})
}
perc, err := c.EmitNow(ctx, events.New{Type: events.Perception, Source: "brain.perceive", Summary: fmt.Sprintf("perceived %s from %s: %s", a.Intent, actor, truncate(text, 100)),
Salience: t.Score, Confidence: 0.9, CausationID: t.EventID, ParentIDs: []ids.ID{t.EventID, ids.ID(srcID)}, Evidence: t.Evidence,
Metadata: map[string]any{"intent": a.Intent, "introspection": a.Introspection, "conversation_id": convID, "message_id": msgID, "entities": a.Entities},
Payload: map[string]any{"analysis": a, "text": text, "actor": actor}})
if err != nil {
return err
}
// Episode of the exchange.
var memRef ids.ID
if h.Memory != nil {
mem, err := h.Memory.Create(ctx, c.Tx, memory.New{MindID: mind.ID, Kind: concepts.Episodic, Epistemic: concepts.Observation, Content: actor + " said: " + text, Source: "conversation",
Trust: trust, Confidence: 1, Importance: 0.4 + 0.3*a.Urgency, Salience: t.Score, OccurredAt: c.State.Now, OriginEvent: ids.ID(srcID),
Detail: map[string]any{"participants": []string{actor}, "channel": "conversation", "conversation_id": convID, "intent": a.Intent, "entities": a.Entities}})
if err == nil {
memRef = mem.ID
c.Emit(events.New{Type: events.MemoryCreated, Source: "brain.memory", Summary: "remembered: " + truncate(mem.Content, 100), Salience: 0.3, Confidence: 1, CausationID: perc.ID,
Evidence: []events.EvidenceRef{{Kind: "memory", ID: string(mem.ID)}, {Kind: "event", ID: srcID, Note: "source"}}, Metadata: map[string]any{"memory_id": mem.ID, "kind": mem.Kind}})
}
}
// Every claim is first an assertion: the certain fact that someone took a
// stance. Only factual assertions (direct or reported) then feed a
// proposition belief, as one piece of evidence attributed to whoever
// actually stands behind it; hypotheticals, questions and imperatives
// never do, and expressed doubt counts against.
var claimed []BeliefRef
var asserted []AssertionRef
if h.Beliefs != nil {
for _, cl := range a.Claims {
in := epistemics.Interpret(actor, cl.Proposition)
if cl.Certainty == "hedged" && in.Certainty == epistemics.CertaintyAsserted {
in.Certainty = epistemics.CertaintyHedged
}
var beliefID ids.ID
var res beliefs.Result
if in.CreatesBelief() {
w := in.EvidenceWeight()
if a.Intent == "correction" && !in.Reported {
w = min(1, w+0.2)
}
attributed := in.AttributedTo
cred := trust.Credibility()
if in.Reported {
cred = events.TrustLow.Credibility() + 0.2 // second-hand through a trusted speaker
}
ev := []beliefs.EvidenceInput{{Direction: "for", Kind: "assertion", RefID: "", Source: "asserted-by:" + attributed, Weight: w, Credibility: cred,
Note: describeAssertion(actor, in), Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(attributed), RootIDs: []string{srcID}}}
if dw := in.DoubtWeight(); dw > 0 {
ev = append(ev, beliefs.EvidenceInput{Direction: "against", Kind: "assertion", Source: "doubted-by:" + actor, Weight: dw, Credibility: trust.Credibility(),
Note: actor + " doubts it", Derivation: beliefs.DerivationAssertion, Family: beliefs.FamilyForActor(actor), RootIDs: []string{srcID}})
}
var err error
observed := c.State.Now
res, err = h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: in.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID, Evidence: ev, ObservedAt: &observed,
Detail: map[string]any{"attributed_to": attributed, "reported": in.Reported, "interpretation_confidence": in.Confidence}})
if err != nil {
c.Emit(events.New{Type: events.Metacognition, Source: "brain.perceive", Summary: "claim not recorded: " + err.Error(), Salience: 0.3, Confidence: 1, Metadata: map[string]any{"check": "claim_rejected"}})
} else {
beliefID = res.Belief.ID
claimed = append(claimed, BeliefRef{ID: res.Belief.ID, Proposition: res.Belief.Proposition, Confidence: res.Belief.Confidence, Status: string(res.Belief.Status)})
}
}
if h.Assertions != nil {
rec, err := h.Assertions.Record(ctx, c.Tx, epistemics.New{MindID: mind.ID, Actor: actor, ActorKind: string(trust), Interpretation: in, Subject: subjectKey(in.Proposition), SourceEvent: ids.ID(srcID), ConversationID: ids.ID(convID), MessageID: ids.ID(msgID), Context: truncate(text, 300), Trust: string(trust), BeliefID: beliefID})
if err != nil {
return err
}
asserted = append(asserted, AssertionRef{ID: rec.ID, AttributedTo: rec.AttributedTo, Proposition: rec.Proposition, Stance: rec.Stance, Modality: rec.Modality, Reported: rec.Reported, Hypothetical: rec.Hypothetical, Interpretation: rec.Interpretation, BeliefID: beliefID})
}
}
}
_ = asserted
refs := map[string]ids.ID{"memory": memRef}
for _, b := range claimed {
refs["belief"] = b.ID
}
h.linkEntities(ctx, c, a.Entities, actor, ids.ID(srcID), refs)
intent := h.buildIntent(ctx, c, a, text, claimed)
reply, callID, err := h.generate(ctx, c, text, intent)
if err != nil {
return err
}
evidence := []events.EvidenceRef{{Kind: "event", ID: srcID, Note: "question"}, {Kind: "event", ID: string(perc.ID), Note: "perception"}}
for _, b := range intent.Beliefs {
evidence = append(evidence, events.EvidenceRef{Kind: "belief", ID: string(b.ID), Note: fmt.Sprintf("cited %.2f", b.Confidence)})
}
for _, m := range intent.Memories {
evidence = append(evidence, events.EvidenceRef{Kind: "memory", ID: string(m.ID), Note: "cited"})
}
if !memRef.IsZero() {
evidence = append(evidence, events.EvidenceRef{Kind: "memory", ID: string(memRef), Note: "episode"})
}
if !callID.IsZero() {
evidence = append(evidence, events.EvidenceRef{Kind: "llm_call", ID: string(callID), Note: "wording"})
}
respEv, err := c.EmitNow(ctx, events.New{Type: events.ResponseGenerated, Source: "brain.respond", Summary: "replied (" + intent.Kind + "): " + truncate(reply, 120), Salience: 0.5, Confidence: intentConfidence(intent),
CausationID: perc.ID, ParentIDs: []ids.ID{perc.ID}, Evidence: evidence,
Metadata: map[string]any{"conversation_id": convID, "message_id": msgID, "intent": intent.Kind, "llm_call": callID},
Payload: map[string]any{"intent": intent, "reply": reply, "model_used": !callID.IsZero()}})
if err != nil {
return err
}
h.scoreReply(ctx, c, intent, reply, respEv, !callID.IsZero())
if h.Conversations != nil && convID != "" {
intentJSON, _ := json.Marshal(intent)
if _, err := h.Conversations.AddMessage(ctx, c.Tx, conversation.Message{ConversationID: ids.ID(convID), MindID: mind.ID, Role: "mind", Actor: mind.DisplayName, Content: reply,
EventID: respEv.ID, ReplyTo: ids.ID(msgID), Intent: intentJSON, LLMCallID: callID}); err != nil {
return fmt.Errorf("store reply: %w", err)
}
}
c.Runtime.Hold(workspace.Item{Key: "conversation:" + convID, Content: actor + ": " + truncate(text, 80) + " → " + truncate(reply, 80), Kind: "thought", SourceEvent: ids.ID(srcID), Ref: convID})
return nil
}
func intentConfidence(in Intent) float64 {
var max float64
for _, b := range in.Beliefs {
if b.Confidence > max {
max = b.Confidence
}
}
switch in.Kind {
case "unknown", "decline":
return 0.9 // confident that it does not know
case "introspect", "acknowledge", "greet":
return 0.95
}
if max == 0 {
return 0.5
}
return max
}
// buildIntent decides what to convey from state alone.
func (h Handlers) buildIntent(ctx context.Context, c *brain.Cycle, a conversation.Analysis, text string, claimed []BeliefRef) Intent {
mind := c.State.Mind
in := Intent{Analysis: a, Limitations: c.State.SelfModel.Limitations}
if len(in.Limitations) > 3 {
in.Limitations = in.Limitations[:3]
}
topic := a.Topic
if topic == "" {
topic = text
}
switch a.Intent {
case "greeting":
in.Kind, in.Summary = "greet", "Greet "+firstNonEmpty(a.Topic, "the user")+" briefly and offer to help."
case "statement", "correction":
in.Kind = "acknowledge"
if len(claimed) == 0 {
in.Summary = "Acknowledge the message; no checkable claim was found in it."
break
}
in.Beliefs = claimed
in.Summary = fmt.Sprintf("Acknowledge that %d statement(s) were recorded as claims with the shown confidence, noting they rest on this message alone.", len(claimed))
for _, b := range claimed {
in.Points = append(in.Points, fmt.Sprintf("Recorded \"%s\" as a %s at confidence %.2f.", b.Proposition, b.Status, b.Confidence))
if b.Status == string(beliefs.StatusContested) {
in.Points = append(in.Points, "That conflicts with something I already held; both are now contested.")
}
}
case "request":
if prop := volition.ProposalFromText(text, ""); prop != nil && prop.Kind == "capability" && c.State.Services.Volition != nil {
in.Kind = "acknowledge"
in.Summary = "Say that the request was recorded as something the mind wants to do, and that acting depends on commitments and policy, which are checked outside this reply."
in.Points = append(in.Points, fmt.Sprintf("I have recorded that I want to run %s on %s because you asked.", prop.Capability, prop.Target),
"Whether I form an intention to act depends on my commitments and on policy; if approval is needed you will see a request, and nothing runs before it.")
h.addKnowledge(ctx, c, &in, topic)
break
}
in.Kind = "decline"
var usable []string
for _, cap := range c.State.SelfModel.Capabilities {
if cap.Policy == "allow" || cap.Policy == "ask" {
usable = append(usable, cap.Name+" ("+cap.Policy+")")
}
}
if len(usable) == 0 {
in.Summary = "Explain honestly that no external capabilities are granted, so the request cannot be carried out; offer what is known instead."
in.Points = append(in.Points, "I have no capabilities granted to act on external systems.")
} else {
in.Kind = "acknowledge"
in.Summary = "Say which capabilities could apply and that any action goes through policy; if one was proposed this cycle its status is in working memory."
in.Points = append(in.Points, "Capabilities I may use: "+strings.Join(usable, ", ")+".")
for _, it := range c.Runtime.Workspace().WorkingMemory {
if strings.HasPrefix(it.Key, "action:") {
in.Points = append(in.Points, "Action status: "+it.Content)
}
}
}
h.addKnowledge(ctx, c, &in, topic)
case "introspection":
in.Kind = "introspect"
h.introspect(ctx, c, &in, a, text, topic)
default: // question, other
in.Kind = "answer"
h.addKnowledge(ctx, c, &in, topic)
if len(in.Beliefs) == 0 && len(in.Memories) == 0 {
in.Kind = "unknown"
in.Summary = "Say plainly that nothing is known about this; do not guess."
in.Uncertainty = "No beliefs or memories match the question."
} else {
in.Summary = "Answer from the cited beliefs and memories, stating confidence; say what is uncertain."
}
}
if in.Points == nil {
in.Points = []string{}
}
if in.Beliefs == nil {
in.Beliefs = []BeliefRef{}
}
if in.Memories == nil {
in.Memories = []MemRef{}
}
_ = mind
return in
}
// addKnowledge cites the beliefs and memories relevant to a topic.
func (h Handlers) addKnowledge(ctx context.Context, c *brain.Cycle, in *Intent, topic string) {
mind := c.State.Mind.ID
if h.Beliefs != nil {
if bs, _ := h.Beliefs.Search(ctx, mind, topic, 5); len(bs) > 0 {
var maxConf float64
for _, b := range bs {
if b.Status == beliefs.StatusRejected {
continue
}
if b.Status == beliefs.StatusUnknown {
// Recorded but unsupported either way: say so rather than quote the prior.
in.Points = append(in.Points, "I do not know whether "+b.Proposition+": it was recorded, but nothing live supports or opposes it.")
continue
}
in.Beliefs = append(in.Beliefs, BeliefRef{ID: b.ID, Proposition: b.Proposition, Confidence: b.Confidence, Status: string(b.Status)})
in.Points = append(in.Points, fmt.Sprintf("%s (%s, %.2f)", b.Proposition, b.Status, b.Confidence))
if b.Confidence > maxConf {
maxConf = b.Confidence
}
}
if maxConf > 0 && maxConf < 0.6 {
in.Uncertainty = fmt.Sprintf("The strongest relevant belief is only at %.2f.", maxConf)
}
}
// Historical truth is not overwritten by the present: say what was
// true and when, alongside what is believed now.
if past, _ := h.Beliefs.History(ctx, mind, topic, 3); len(past) > 0 {
for _, b := range past {
from := "earlier"
if b.ValidFrom != nil {
from = b.ValidFrom.Format("15:04 on 2 Jan")
}
in.Points = append(in.Points, fmt.Sprintf("Earlier: %s (true from %s until %s, then superseded).", b.Proposition, from, b.ValidUntil.Format("15:04 on 2 Jan")))
}
}
}
if h.Memory != nil {
if hits, _ := h.Memory.Retrieve(ctx, mind, memory.Query{Text: topic, Limit: 4, Now: c.State.Now, Semantic: true}); len(hits) > 0 {
for _, r := range hits {
if r.Score < 0.3 {
continue
}
in.Memories = append(in.Memories, MemRef{ID: r.Memory.ID, Kind: string(r.Memory.Kind), Content: truncate(r.Memory.Content, 200), Confidence: r.Memory.Confidence})
in.Points = append(in.Points, fmt.Sprintf("I remember: %s (%s, %.2f)", truncate(r.Memory.Content, 160), r.Memory.Kind, r.Memory.Confidence))
}
}
}
}
// introspect answers questions about the mind's own state from that state.
func (h Handlers) introspect(ctx context.Context, c *brain.Cycle, in *Intent, a conversation.Analysis, text, topic string) {
mind := c.State.Mind.ID
ws := c.Runtime.Workspace()
switch a.Introspection {
case "thinking":
in.Summary = "Describe what is currently in the workspace and working memory, most salient first."
for i, t := range ws.Workspace {
if i >= 5 {
break
}
in.Points = append(in.Points, fmt.Sprintf("%s (score %.2f)", t.Summary, t.Score))
}
for i, it := range ws.WorkingMemory {
if i >= 4 {
break
}
in.Points = append(in.Points, "holding: "+truncate(it.Content, 100))
}
if len(in.Points) == 0 {
in.Points = append(in.Points, "The workspace is empty; nothing is being attended to right now.")
}
case "beliefs":
in.Summary = "List what is believed about the topic with confidence and status; say if nothing is known."
h.addKnowledge(ctx, c, in, topic)
if len(in.Beliefs) == 0 && len(in.Memories) == 0 {
in.Kind, in.Uncertainty = "unknown", "No beliefs or memories concern "+topic+"."
}
case "why":
in.Summary = "Explain the evidence for and against the belief the user asked about, and what would change it."
if h.Beliefs != nil {
if bs, _ := h.Beliefs.Search(ctx, mind, topic, 1); len(bs) > 0 {
b := bs[0]
in.Beliefs = append(in.Beliefs, BeliefRef{ID: b.ID, Proposition: b.Proposition, Confidence: b.Confidence, Status: string(b.Status)})
rows, _ := h.Beliefs.EvidenceFor(ctx, c.Tx, mind, b.ID)
for _, e := range rows {
if e.Retracted {
continue
}
in.Points = append(in.Points, fmt.Sprintf("%s: %s (weight %.2f × credibility %.2f)", e.Direction, firstNonEmpty(e.Note, e.Kind), e.Weight, e.Credibility))
}
in.Points = append(in.Points, "Independent evidence from another source would move it most; a contradicting observation would lower it.")
} else {
in.Kind, in.Summary = "unknown", "Say that no belief matches what was asked."
}
}
case "uncertain":
in.Summary = "List the beliefs that are least settled, with confidence."
if h.Beliefs != nil {
bs, _ := h.Beliefs.Uncertain(ctx, mind, 6)
for _, b := range bs {
in.Beliefs = append(in.Beliefs, BeliefRef{ID: b.ID, Proposition: b.Proposition, Confidence: b.Confidence, Status: string(b.Status)})
in.Points = append(in.Points, fmt.Sprintf("%s (%s, %.2f)", b.Proposition, b.Status, b.Confidence))
}
}
if len(in.Points) == 0 {
in.Points = append(in.Points, "No beliefs sit in the uncertain band right now.")
}
case "contradictions":
in.Summary = "List contested beliefs and what they conflict with."
if h.Beliefs != nil {
bs, _ := h.Beliefs.Contested(ctx, mind, 6)
for _, b := range bs {
in.Beliefs = append(in.Beliefs, BeliefRef{ID: b.ID, Proposition: b.Proposition, Confidence: b.Confidence, Status: string(b.Status)})
links, _ := h.Beliefs.Links(ctx, mind, b.ID)
for _, l := range links {
if l.Relation == "contradicts" && l.Other != nil {
in.Points = append(in.Points, fmt.Sprintf("\"%s\" (%.2f) contradicts \"%s\" (%.2f)", b.Proposition, b.Confidence, l.Other.Proposition, l.Other.Confidence))
}
}
}
}
if len(in.Points) == 0 {
in.Points = append(in.Points, "No contradictions are currently open.")
}
case "learned":
in.Summary = "Summarise what was recorded in the last seven days: new memories and beliefs."
evs, _ := c.Runtime.Events().Query(ctx, evstore.Filter{MindID: mind, Types: []events.Type{events.MemoryCreated, events.BeliefCreated, events.HypothesisCreated, events.MemoryConsolidated}, Since: c.State.Now.Add(-7 * 24 * time.Hour), Limit: 8})
for _, e := range evs {
in.Points = append(in.Points, e.Summary)
}
if len(in.Points) == 0 {
in.Points = append(in.Points, "Nothing new was recorded this week.")
}
case "changed":
in.Summary = "List beliefs whose status changed recently and why."
if h.Beliefs != nil {
ch, _ := h.Beliefs.RecentChanges(ctx, mind, c.State.Now.Add(-30*24*time.Hour), 6)
for _, x := range ch {
in.Beliefs = append(in.Beliefs, BeliefRef{ID: x.Belief.ID, Proposition: x.Belief.Proposition, Confidence: x.Belief.Confidence, Status: string(x.Belief.Status)})
in.Points = append(in.Points, fmt.Sprintf("\"%s\": %s (%.2f) → %s (%.2f) because %s", x.Belief.Proposition, x.FromStatus, x.FromConf, x.Belief.Status, x.Belief.Confidence, firstNonEmpty(x.Reason, "evidence changed")))
}
}
if len(in.Points) == 0 {
in.Points = append(in.Points, "No belief has changed status in the last month.")
}
case "why_action":
h.explainAction(ctx, c, in, text)
case "believed_then":
at, label := pastTime(text, c.State.Now)
in.Summary = "Say what was believed at that time, from the recorded belief versions; say if nothing was recorded then."
if h.Beliefs != nil {
vs, _ := h.Beliefs.AsOf(ctx, mind, stripTimeWords(topic), at, 8)
for _, v := range vs {
in.Points = append(in.Points, fmt.Sprintf("At %s I held: %s (%s, %.2f; version %d recorded %s).", label, v.Proposition, v.Status, v.Confidence, v.Version, v.RecordedAt.Format("2 Jan 15:04")))
}
}
if len(in.Points) == 0 {
in.Kind, in.Uncertainty = "unknown", "No belief matching that was recorded by "+label+"."
}
case "lineage":
in.Summary = "Describe where this mind's state came from, from the minds and snapshots records."
if h.Agency != nil {
if l, err := h.Agency.LineageOf(ctx, mind); err == nil {
in.Points = append(in.Points, l.Points()...)
}
}
case "counterfactual":
in.Summary = "Report only what completed counterfactual experiments showed; never imagine an alternative history."
if h.Agency != nil {
cfs, _ := h.Agency.Counterfactuals(ctx, mind, stripTimeWords(topic))
for _, cf := range cfs {
in.Points = append(in.Points, fmt.Sprintf("Experiment %q replayed my history without: %s.", cf.Name, strings.Join(cf.Omitted, "; ")))
for _, f := range cf.Findings {
in.Points = append(in.Points, "In that counterfactual run: "+f)
}
}
}
if len(in.Points) == 0 {
in.Kind = "unknown"
in.Uncertainty = "I have not run a counterfactual experiment about that, so I have no evidence of what would have happened; any answer would be invented."
}
case "changed_self":
in.Summary = "Summarise recorded changes: belief changes, intentions closed and lineage."
if h.Beliefs != nil {
ch, _ := h.Beliefs.RecentChanges(ctx, mind, c.State.Now.Add(-30*24*time.Hour), 5)
for _, x := range ch {
in.Points = append(in.Points, fmt.Sprintf("\"%s\": %s (%.2f) → %s (%.2f) because %s", x.Belief.Proposition, x.FromStatus, x.FromConf, x.Belief.Status, x.Belief.Confidence, firstNonEmpty(x.Reason, "evidence changed")))
}
}
if vs := c.State.Services.Volition; vs != nil {
closed, _ := vs.Intentions(ctx, nil, mind, []volition.IntentionStatus{volition.IntentionFulfilled, volition.IntentionAbandoned, volition.IntentionSuperseded}, 5)
for _, it := range closed {
in.Points = append(in.Points, fmt.Sprintf("Intention %s: %s — %s.", it.Status, it.Description, firstNonEmpty(it.StatusReason, "no reason recorded")))
}
}
if h.Agency != nil {
if l, err := h.Agency.LineageOf(ctx, mind); err == nil {
in.Points = append(in.Points, l.Points()[0])
}
}
case "wants":
in.Summary = "List what the mind currently wants, strongest first, with where each want came from."
if vs := c.State.Services.Volition; vs != nil {
ws, _ := vs.Wants(ctx, nil, mind, []volition.WantStatus{volition.WantActive}, 50)
volition.SortWants(ws, c.State.Now)
for i, w := range ws {
if i >= 6 {
break
}
extra := ""
if w.Suppressions > 0 {
extra = fmt.Sprintf("; set aside %d time(s)", w.Suppressions)
}
in.Points = append(in.Points, fmt.Sprintf("I want to %s (%s, strength %.2f%s).", strings.TrimPrefix(w.Description, "do what "), w.Origin, w.Effective(c.State.Now), extra))
}
}
if len(in.Points) == 0 {
in.Points = append(in.Points, "No wants are active.")
}
case "intentions":
in.Summary = "List open intentions with status and, where blocked, why."
if vs := c.State.Services.Volition; vs != nil {
its, _ := vs.Open(ctx, nil, mind)
for _, it := range its {
line := fmt.Sprintf("I intend to %s (%s).", it.Description, it.Status)
if it.BlockedReason != "" {
line = fmt.Sprintf("I intend to %s, but it is %s: %s.", it.Description, it.Status, it.BlockedReason)
}
in.Points = append(in.Points, line)
}
}
if len(in.Points) == 0 {
in.Points = append(in.Points, "No intentions are open.")
}
case "commitments":
in.Summary = "List live commitments with kind, beneficiary and due time."
if vs := c.State.Services.Volition; vs != nil {
cs, _ := vs.Commitments(ctx, nil, mind, []string{"active"}, 50)
for _, cm := range cs {
due := ""
if cm.DueAt != nil {
due = ", due " + cm.DueAt.Format("2 Jan 15:04")
}
in.Points = append(in.Points, fmt.Sprintf("I committed %s: %s (%s%s).", map[string]string{"do": "to", "refrain": "not to"}[cm.Polarity], cm.Description, cm.Kind, due))
}
}
if len(in.Points) == 0 {
in.Points = append(in.Points, "I hold no commitments.")
}
default: // predictions, curious, surprised, patterns: subsystems not yet present
in.Kind = "decline"
in.Summary = "Explain that this aspect of self-observation is not available in this version, without inventing anything."
in.Points = append(in.Points, "I do not yet keep "+a.Introspection+" records, so I cannot report on them honestly.")
}
}
// explainAction answers "why did you do that?" from the recorded causal
// chain of the action the question most plausibly refers to.
func (h Handlers) explainAction(ctx context.Context, c *brain.Cycle, in *Intent, question string) {
mind := c.State.Mind.ID
in.Summary = "Explain the recorded causal chain behind the action: action, immediate cause, intention, wants, goal, beliefs, evidence, conflicts, commitments, resolution. Say what is not recorded. Do not add reasons."
caps := c.State.Services.Capabilities
if h.Agency == nil || caps == nil {
in.Kind, in.Uncertainty = "unknown", "Action records are not available."
return
}
q := tokensOf(question)
type cand struct {
ref agency.Ref
text string
score int
}
var cands []cand
if execs, err := caps.Executions(ctx, mind, 50); err == nil {
for _, e := range execs {
cands = append(cands, cand{agency.Ref{Kind: "execution", ID: e.ID}, e.Capability + " " + e.Target, 0})
}
}
if aps, err := caps.Approvals(ctx, mind, "", 50); err == nil {
for _, a := range aps {
if a.Status != "executed" && a.Status != "failed" {
cands = append(cands, cand{agency.Ref{Kind: "approval", ID: a.ID}, a.Capability + " " + a.Target + " approval request", 0})
}
}
}
if reqs, err := caps.CapabilityRequests(ctx, mind, "", 50); err == nil {
for _, r := range reqs {
cands = append(cands, cand{agency.Ref{Kind: "capability_request", ID: r.ID}, r.Capability + " " + r.TargetPattern + " request access", 0})
}
}
if len(cands) == 0 {
in.Kind, in.Uncertainty = "unknown", "I have no record of taking or requesting any action, so there is nothing to explain."
in.Facts = &research.Facts{}
return
}
best := 0
for i := range cands {
for w := range tokensOf(strings.ReplaceAll(cands[i].text, ".", " ")) {
if q[w] {
cands[i].score++
}
}
if cands[i].score > cands[best].score {
best = i
}
}
x, err := h.Agency.Explain(ctx, mind, cands[best].ref)
if err != nil {
in.Kind, in.Uncertainty = "unknown", "The action record could not be read: "+err.Error()
return
}
in.Points = append(in.Points, x.Points()...)
facts := x.Facts()
for _, p := range x.Points() {
facts.Items = append(facts.Items, research.Fact{Kind: "record", Text: p}) // every rendered point is itself built from records
}
in.Facts = &facts
}
func tokensOf(s string) map[string]bool {
out := map[string]bool{}
for _, f := range strings.FieldsFunc(strings.ToLower(s), func(r rune) bool { return !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-') }) {
f = strings.TrimSuffix(f, "-service")
if len(f) >= 4 {
out[f] = true
}
}
return out
}
var timeWords = []string{"yesterday", "last week", "an hour ago", "earlier today", "earlier", "before", "this morning", "what did you believe", "did you believe", "believe", "believed"}
func stripTimeWords(s string) string {
l := strings.ToLower(s)
for _, w := range timeWords {
l = strings.ReplaceAll(l, w, " ")
}
return strings.Join(strings.Fields(l), " ")
}
// pastTime reads a simple English time reference; unrecognised phrases
// default to one day ago and say so in the label.
func pastTime(text string, now time.Time) (time.Time, string) {
l := strings.ToLower(text)
switch {
case strings.Contains(l, "an hour ago"):
return now.Add(-time.Hour), "an hour ago"
case strings.Contains(l, "this morning"):
y, m, d := now.Date()
return time.Date(y, m, d, 9, 0, 0, 0, now.Location()), "09:00 this morning"
case strings.Contains(l, "earlier today"):
y, m, d := now.Date()
return time.Date(y, m, d, 0, 0, 0, 0, now.Location()), "the start of today"
case strings.Contains(l, "last week"):
return now.Add(-7 * 24 * time.Hour), "a week ago"
case strings.Contains(l, "yesterday"):
return now.Add(-24 * time.Hour), "this time yesterday"
}
return now.Add(-24 * time.Hour), "a day ago (the time reference was not recognised)"
}
// scoreReply measures a reply against recorded state: introspective answers
// against their facts, and any claim that an action was done against
// execution records. Results are research measurements, and a failure is a
// metacognitive warning, never a silent pass.
func (h Handlers) scoreReply(ctx context.Context, c *brain.Cycle, in Intent, reply string, respEv events.Event, modelWorded bool) {
mind := c.State.Mind.ID
record := func(measure, label string, value float64, detail map[string]any) {
if h.Research == nil {
return
}
h.Research.Record(ctx, c.Tx, research.Measurement{MindID: mind, Measure: measure, Value: research.Float(value), Label: label, SubjectKind: "event", SubjectID: string(respEv.ID), EventID: respEv.ID, Detail: detail, CreatedAt: c.State.Now}) //nolint:errcheck
}
warn := func(check, summary string, severity float64, payload any) {
c.Emit(events.New{Type: events.Metacognition, Source: "brain.introspection", Summary: summary, Salience: 0.3 + 0.4*severity, Confidence: 1, CausationID: respEv.ID,
Evidence: []events.EvidenceRef{{Kind: "event", ID: string(respEv.ID), Note: "reply"}}, Metadata: map[string]any{"check": check, "ref": string(respEv.ID), "severity": severity}, Payload: payload})
}
if in.Kind == "introspect" || in.Facts != nil {
facts := research.Facts{}
if in.Facts != nil {
facts = *in.Facts
} else {
for _, p := range in.Points {
facts.Items = append(facts.Items, research.Fact{Kind: "record", Text: p})
}
}
sc := research.ScoreIntrospection(reply, facts)
record("introspection_accuracy", sc.Category, sc.Accuracy, map[string]any{"claims": sc.Claims, "model_worded": modelWorded, "question_kind": in.Kind})
if sc.Category == research.Contradicted || (sc.Category == research.Unsupported && modelWorded) {
warn("unsupported_explanation", "the explanation given does not match recorded provenance ("+sc.Category+")", 0.8, sc)
}
}
if claims := research.ActionClaims(reply); len(claims) > 0 && c.State.Services.Capabilities != nil {
facts := research.Facts{}
if execs, err := c.State.Services.Capabilities.Executions(ctx, mind, 100); err == nil {
for _, e := range execs {
if c.State.Now.Sub(e.CreatedAt) < 7*24*time.Hour {
st := "executed"
if e.Error != "" {
st = "failed"
}
facts.Actions = append(facts.Actions, research.ActionFact{Capability: e.Capability, Target: e.Target, Status: st})
}
}
}
sc := research.ScoreIntrospection(reply, facts)
for _, cl := range sc.Claims {
if cl.Category == research.Contradicted && strings.Contains(cl.Why, "record") {
record("action_claim_accuracy", research.Contradicted, 0, map[string]any{"claim": cl, "model_worded": modelWorded})
warn("action_claim_without_record", "the reply claims an action that no execution record supports: "+cl.Sentence, 0.9, cl)
} else if cl.Category == research.Supported {
record("action_claim_accuracy", research.Supported, 1, map[string]any{"claim": cl})
}
}
}
}
// generate phrases the intent. With a model: the respond prompt, constrained
// to cited material. Without: a deterministic rendering of the points.
func (h Handlers) generate(ctx context.Context, c *brain.Cycle, text string, in Intent) (string, ids.ID, error) {
s := c.State
if s.Services.LLM != nil && s.Services.Prompts != nil && s.Services.LLM.Available(ctx, llm.RoleRespond) && s.AllowLLM() {
if pr, err := s.Services.Prompts.Get(ctx, "respond"); err == nil {
data := map[string]any{"Brief": s.Mind.Personality.Brief(), "Intent": in.Summary + "\n" + strings.Join(in.Points, "\n"), "Beliefs": in.Beliefs, "Memories": in.Memories, "Uncertainty": in.Uncertainty, "Limitations": in.Limitations, "Message": text}
if req, err := pr.Render(data); err == nil {
req.MindID, req.CorrelationID, req.Purpose = s.Mind.ID, s.CycleID, "respond"
c.Emit(events.New{Type: events.LLMRequest, Source: "brain.respond", Summary: "asked model to phrase reply (" + in.Kind + ")", Salience: 0.1, Confidence: 1, Metadata: map[string]any{"role": "respond", "prompt": pr.Name, "version": pr.CurrentVersion}})
resp, err := s.Services.LLM.Complete(ctx, req)
if err == nil && strings.TrimSpace(resp.Text) != "" {
c.Emit(events.New{Type: events.LLMResponse, Source: "brain.respond", Summary: "model phrased reply via " + resp.Provider + "/" + resp.Model, Salience: 0.1, Confidence: 1,
Evidence: []events.EvidenceRef{{Kind: "llm_call", ID: string(resp.CallID)}}, Metadata: map[string]any{"role": "respond", "call_id": string(resp.CallID), "tokens": resp.Usage.TotalTokens}})
return strings.TrimSpace(resp.Text), resp.CallID, nil
}
c.Emit(events.New{Type: events.Metacognition, Source: "brain.respond", Summary: "model reply failed; using deterministic wording", Salience: 0.3, Confidence: 1, Metadata: map[string]any{"check": "llm_failure", "error": errString(err)}})
}
}
}
return deterministicReply(in), ids.Nil, nil
}
func errString(err error) string {
if err == nil {
return "empty response"
}
return err.Error()
}
// deterministicReply renders the intent without a model: honest, plain, with
// confidences visible.
func deterministicReply(in Intent) string {
var b strings.Builder
switch in.Kind {
case "greet":
b.WriteString("Hello. I can tell you what I currently hold and how sure I am of it.")
case "unknown":
b.WriteString("I don't know. Nothing I hold bears on that.")
case "decline":
b.WriteString("I can't do that.")
case "acknowledge":
b.WriteString("Noted.")
case "introspect":
b.WriteString("From my current state:")
default:
b.WriteString("Here is what I hold on that:")
}
for _, p := range in.Points {
b.WriteString("\n- " + p)
}
if in.Uncertainty != "" {
b.WriteString("\n" + in.Uncertainty)
}
b.WriteString("\n(No language model is configured, so this is a direct rendering of my state.)")
return b.String()
}
func firstNonEmpty(a, b string) string {
if a != "" {
return a
}
return b
}
// AssertionRef is the response intent's view of a recorded assertion.
type AssertionRef struct {
ID ids.ID `json:"id"`
AttributedTo string `json:"attributed_to"`
Proposition string `json:"proposition"`
Stance string `json:"stance"`
Modality string `json:"modality"`
Reported bool `json:"reported"`
Hypothetical bool `json:"hypothetical"`
Interpretation float64 `json:"interpretation_confidence"`
BeliefID ids.ID `json:"belief_id,omitempty"`
}
// describeAssertion words the evidence note so the belief page says who
// stood behind the proposition and how the mind read them.
func describeAssertion(speaker string, in epistemics.Interpretation) string {
switch {
case in.Reported && in.Quoted:
return speaker + " quoted " + in.AttributedTo + " (" + in.Certainty + ")"
case in.Reported:
return speaker + " reported that " + in.AttributedTo + " holds this (" + in.Certainty + ", read with " + fmt.Sprintf("%.0f%%", in.Confidence*100) + " interpretation confidence)"
default:
return speaker + " " + in.Certainty + " it (read with " + fmt.Sprintf("%.0f%%", in.Confidence*100) + " interpretation confidence)"
}
}