Volition and planning now run as part of cognition (PLAN §8.2 item 3):
- A volition hook derives wants after each cycle from what it perceived
and produced, satisfies answer wants when a reply is sent, raises due,
overdue, expired and conflicting commitments, and expires faded wants.
Events produced by earlier cycles are not re-derived.
- An intend process proposes deciding about the strongest undecided
wants; the candidate competes for the workspace, so attention gates
what the mind decides to pursue. The handler makes value tensions
explicit (caution against disruptive writes, privacy against reading
personal data), resolves them with recorded scores, runs deterministic
formation with commitments, policy preview, goals and values, records
the decision whatever it is, and plans a formed intention in the same
transaction. Operator requests carry their own weight in formation.
- Explicit requests in conversation ("restart <unit>", "logs for <unit>",
"fetch <url>") become wants with proposed means instead of direct
invocations; the reply says the request was recorded and that acting
depends on commitments and policy.
- The executor is woken after commits, runs in a scheduler-owned loop and
reacts to the event bus: a new commitment tightens or abandons affected
intentions (withdrawing their pending approvals), a weakened assumption
or a finished goal triggers reconsideration. A tightened plan may reuse
its strategy; a failed one may not.
- Metacognition gains volition checks: strong wants repeatedly set aside,
intentions with no recorded reason, intentions a commitment forbids,
commitments that cannot be kept, churn, repeated failed execution,
cost outweighing value, and intentions that outlived their goal.
- A plan model role and prompt let a model propose decompositions that
the planner validates. Ablations are a fork-only configuration field.
- Approvals required by an intention are described as coming from a
commitment or value constraint. The expectation after an approved
action is linked to the execution event, not the request.
- The wants.values column of the unreleased migration 0026 is renamed
value_keys: VALUES is reserved and the generic snapshot restore does
not quote identifiers.
- The hostile-output security test now uses a message that asks for no
action, because an operator's "status of <unit>" is legitimately a
read-only status check.
535 lines
25 KiB
Go
535 lines
25 KiB
Go
package cognition
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"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/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"`
|
||
}
|
||
|
||
// 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
|
||
}
|
||
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, 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, 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.")
|
||
}
|
||
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.")
|
||
}
|
||
}
|
||
|
||
// 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)"
|
||
}
|
||
}
|