Phase 12 (ADR-0005). Capabilities are registered in code with parameter schemas, risk classes and default effects; sudo_shell and delete_database are locked to DENY and cannot be relaxed. Policies are scoped globally or per mind with capability, target glob, action kind and optional hour window; evaluation is deterministic, most specific wins, and ties go to the stricter effect. Invoke is the single entry point: it refuses dreaming and replay contexts, validates parameters (unknown keys, non http URLs and path traversal rejected), evaluates policy and session grants, hashes the exact request and records a policy_decision event. ALLOW executes with timeout and panic isolation and turns textual output into a tool-trust observation; ASK creates an approval request bound to the hash with an alert notification; DENY is counted and recorded. Operators approve once, approve for a time-boxed session grant, deny with a note, or modify parameters (re-validated, re-hashed, original kept in the append-only decision log). The self-model's capability list is recomputed from effective policy on start and on every policy change. The act process maps fetch requests to http.fetch and the action handler invokes through the layer; replies report capabilities and outcomes. Policy matrix and approval centre pages, dashboard count, API, CLI (approvals, policy, invoke) and docs/capabilities.md.
451 lines
20 KiB
Go
451 lines
20 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"
|
||
evstore "github.com/andydixon/noema/internal/events"
|
||
"github.com/andydixon/noema/internal/llm"
|
||
"github.com/andydixon/noema/internal/memory"
|
||
"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}})
|
||
}
|
||
}
|
||
// Claims become beliefs; the message is their evidence.
|
||
var claimed []BeliefRef
|
||
if h.Beliefs != nil {
|
||
for _, cl := range a.Claims {
|
||
w := 0.6
|
||
if cl.Certainty == "hedged" {
|
||
w = 0.3
|
||
}
|
||
if a.Intent == "correction" {
|
||
w += 0.2
|
||
}
|
||
res, err := h.Beliefs.Create(ctx, c.Tx, emitter(c), beliefs.New{MindID: mind.ID, Proposition: cl.Proposition, Epistemic: concepts.Claim, Source: "conversation", OriginEvent: perc.ID, Causation: perc.ID, Correlation: c.State.CycleID,
|
||
Evidence: []beliefs.EvidenceInput{{Direction: "for", Kind: "event", RefID: srcID, Source: "said-by:" + actor, Weight: w, Credibility: trust.Credibility(), Note: actor + " " + cl.Certainty + " it"}}})
|
||
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"}})
|
||
continue
|
||
}
|
||
claimed = append(claimed, BeliefRef{ID: res.Belief.ID, Proposition: res.Belief.Proposition, Confidence: res.Belief.Confidence, Status: string(res.Belief.Status)})
|
||
}
|
||
}
|
||
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":
|
||
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
|
||
}
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
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
|
||
}
|