feat(cognition): wanting, intending and acting inside the cognitive cycle
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.
This commit is contained in:
@@ -3,33 +3,29 @@ package cognition
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/andydixon/noema/internal/attention"
|
||||
"github.com/andydixon/noema/internal/brain"
|
||||
"github.com/andydixon/noema/internal/capabilities"
|
||||
"github.com/andydixon/noema/internal/conversation"
|
||||
"github.com/andydixon/noema/internal/workspace"
|
||||
"github.com/andydixon/noema/internal/volition"
|
||||
"github.com/andydixon/noema/pkg/events"
|
||||
"github.com/andydixon/noema/pkg/ids"
|
||||
)
|
||||
|
||||
// Act proposes capability invocations from requests the mind receives. In
|
||||
// this phase the only deterministic mapping is "fetch <url>" → http.fetch;
|
||||
// integrations register more. Proposing is free; executing goes through the
|
||||
// policy layer, so a request never bypasses ALLOW / ASK / DENY.
|
||||
// Act turns explicit action requests in conversation ("fetch <url>",
|
||||
// "restart <unit>", "logs for <unit>") into wants carrying the proposed
|
||||
// means. It no longer invokes anything: whether the mind intends to act,
|
||||
// under which commitments and with what plan, is decided by formation, and
|
||||
// acting goes through the capability layer, so a request never bypasses
|
||||
// ALLOW / ASK / DENY.
|
||||
type Act struct{}
|
||||
|
||||
// Name implements brain.Process.
|
||||
func (Act) Name() string { return "act" }
|
||||
|
||||
var urlRe = regexp.MustCompile(`https?://[^\s<>"']+`)
|
||||
|
||||
// Observe implements brain.Process.
|
||||
func (Act) Observe(_ context.Context, s *brain.State) ([]brain.Candidate, error) {
|
||||
if s.Services.Capabilities == nil {
|
||||
if s.Services.Volition == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var out []brain.Candidate
|
||||
@@ -41,18 +37,17 @@ func (Act) Observe(_ context.Context, s *brain.State) ([]brain.Candidate, error)
|
||||
if json.Unmarshal(ev.Payload, &p) != nil {
|
||||
continue
|
||||
}
|
||||
a := conversation.Analyse(p.Text)
|
||||
if a.Intent != "request" {
|
||||
if conversation.Analyse(p.Text).Intent != "request" {
|
||||
continue
|
||||
}
|
||||
lower := strings.ToLower(p.Text)
|
||||
if u := urlRe.FindString(p.Text); u != "" && (strings.Contains(lower, "fetch") || strings.Contains(lower, "read") || strings.Contains(lower, "look at") || strings.Contains(lower, "check")) {
|
||||
u = strings.TrimRight(u, ".,)")
|
||||
out = append(out, brain.Candidate{Kind: "action", Subject: "http.fetch:" + u, Summary: "proposed action: fetch " + u,
|
||||
Content: map[string]any{"capability": "http.fetch", "target": u, "params": map[string]any{"url": u}, "reason": "asked by " + p.Actor + ": " + truncate(p.Text, 120), "source_event": string(ev.ID), "conversation_id": ev.Metadata["conversation_id"]},
|
||||
Features: attention.Features{Salience: 0.7, Urgency: 0.6, Consequence: 0.7, Confidence: 0.9, OperatorPriority: prioFor(p.Trust), ContextRelevance: 0.5},
|
||||
Confidence: 0.9, SourceEvents: []ids.ID{ev.ID}, Evidence: []events.EvidenceRef{{Kind: "event", ID: string(ev.ID), Note: "request"}}})
|
||||
prop := volition.ProposalFromText(p.Text, "")
|
||||
if prop == nil || prop.Kind != "capability" {
|
||||
continue
|
||||
}
|
||||
out = append(out, brain.Candidate{Kind: "want", Subject: prop.Capability + ":" + prop.Target, Summary: "wants to " + prop.Capability + " " + prop.Target + " as " + firstNonEmpty(p.Actor, "someone") + " asked",
|
||||
Content: map[string]any{"proposal": *prop, "text": truncate(p.Text, 200), "actor": p.Actor, "operator": p.Trust == events.TrustOperator, "source_event": string(ev.ID), "conversation_id": ev.Metadata["conversation_id"]},
|
||||
Features: attention.Features{Salience: 0.7, Urgency: 0.6, Consequence: 0.7, Confidence: 0.9, OperatorPriority: prioFor(p.Trust), ContextRelevance: 0.5},
|
||||
Confidence: 0.9, SourceEvents: []ids.ID{ev.ID}, Evidence: []events.EvidenceRef{{Kind: "event", ID: string(ev.ID), Note: "request"}}})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -63,38 +58,3 @@ func prioFor(t events.Trust) float64 {
|
||||
}
|
||||
return 0.7
|
||||
}
|
||||
|
||||
// action invokes the capability layer for an admitted action thought and
|
||||
// records the outcome in working memory. The layer decides; this handler
|
||||
// never executes anything itself.
|
||||
func (h Handlers) action(ctx context.Context, c *brain.Cycle, t workspace.Thought) error {
|
||||
svc := c.State.Services.Capabilities
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
capName, _ := t.Content["capability"].(string)
|
||||
target, _ := t.Content["target"].(string)
|
||||
reason, _ := t.Content["reason"].(string)
|
||||
params, _ := t.Content["params"].(map[string]any)
|
||||
var ev []capabilities.EvidenceRef
|
||||
for _, e := range t.Evidence {
|
||||
ev = append(ev, capabilities.EvidenceRef{Kind: e.Kind, ID: e.ID, Note: e.Note})
|
||||
}
|
||||
if c.State.Mode == brain.ModeDreaming {
|
||||
ctx = capabilities.WithForbidden(ctx, "dreaming")
|
||||
}
|
||||
out, err := svc.Invoke(ctx, capabilities.Invocation{MindID: c.State.Mind.ID, Capability: capName, Target: target, Params: params, Reason: reason, Evidence: ev, Correlation: c.State.CycleID})
|
||||
if err != nil {
|
||||
c.Emit(events.New{Type: events.Metacognition, Source: "brain.act", Summary: "action refused before policy: " + err.Error(), Salience: 0.4, Confidence: 1, CausationID: t.EventID, Metadata: map[string]any{"check": "action_invalid", "capability": capName}})
|
||||
return nil
|
||||
}
|
||||
content := fmt.Sprintf("%s on %s: %s", capName, target, out.Status)
|
||||
if out.Status == "pending" {
|
||||
content += " (awaiting operator approval)"
|
||||
}
|
||||
if out.Status == "denied" {
|
||||
content += " (" + out.Decision.Reason + ")"
|
||||
}
|
||||
c.Runtime.Hold(workspace.Item{Key: "action:" + capName + ":" + target, Content: content, Kind: "note", SourceEvent: out.EventID, Ref: string(out.ApprovalID)})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func (g GoalMonitor) Observe(ctx context.Context, s *brain.State) ([]brain.Candi
|
||||
break
|
||||
}
|
||||
out = append(out, brain.Candidate{Kind: "goal", Subject: string(gl.ID), Summary: "goal stalled: " + gl.Title,
|
||||
Content: map[string]any{"goal_id": string(gl.ID), "reason": "no progress for " + stall.String(), "ref": string(gl.ID)},
|
||||
Content: map[string]any{"goal_id": string(gl.ID), "reason": "no progress for " + stall.String(), "ref": string(gl.ID), "stalled": true},
|
||||
Features: attention.Features{Salience: 0.4, GoalRelevance: gl.Priority, Consequence: 0.5 * gl.Priority, Urgency: 0.3, Confidence: 1}, Confidence: 1,
|
||||
Evidence: []events.EvidenceRef{{Kind: "goal", ID: string(gl.ID)}}})
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func (h Handlers) goal(ctx context.Context, c *brain.Cycle, t workspace.Thought)
|
||||
src, _ := t.Content["source_event"].(string)
|
||||
c.Runtime.Hold(workspace.Item{Key: "goal:" + gid, Content: t.Summary, Kind: "goal", Ref: gid, SourceEvent: ids.ID(src)})
|
||||
c.Emit(events.New{Type: events.GoalUpdated, Source: "brain.goals", Summary: t.Summary + " (" + reason + ")", Salience: t.Score, Confidence: 1, CausationID: t.EventID,
|
||||
Evidence: t.Evidence, Metadata: map[string]any{"goal_id": gid, "attended": true}, Payload: map[string]any{"goal_id": gid, "reason": reason}})
|
||||
Evidence: t.Evidence, Metadata: map[string]any{"goal_id": gid, "attended": true, "stalled": t.Content["stalled"] == true}, Payload: map[string]any{"goal_id": gid, "reason": reason}})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ func (h Handlers) Register(r *brain.Registry) {
|
||||
r.Handle("goal", h.goal)
|
||||
r.Handle("question", h.question)
|
||||
r.Handle("metacognition", h.metacognition)
|
||||
r.Handle("action", h.action)
|
||||
r.Handle("want", h.want)
|
||||
r.Handle("intend", h.intend)
|
||||
}
|
||||
|
||||
// emitter adapts the cycle to the beliefs store's Emitter.
|
||||
|
||||
@@ -2,6 +2,7 @@ package cognition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
"github.com/andydixon/noema/internal/beliefs"
|
||||
"github.com/andydixon/noema/internal/brain"
|
||||
evstore "github.com/andydixon/noema/internal/events"
|
||||
"github.com/andydixon/noema/internal/planning"
|
||||
"github.com/andydixon/noema/internal/volition"
|
||||
"github.com/andydixon/noema/internal/workspace"
|
||||
"github.com/andydixon/noema/pkg/events"
|
||||
"github.com/andydixon/noema/pkg/ids"
|
||||
@@ -17,8 +20,9 @@ import (
|
||||
|
||||
// Metacognition audits the mind's own reasoning with deterministic checks
|
||||
// (spec §16): insufficient evidence, single-source reliance, fluent model
|
||||
// output without evidence, repeated failed hypotheses, stale contradictions
|
||||
// and overconfidence against calibration. Warnings compete for attention;
|
||||
// output without evidence, repeated failed hypotheses, stale contradictions,
|
||||
// overconfidence against calibration, and the volition checks in
|
||||
// volitionChecks. Warnings compete for attention;
|
||||
// when admitted they lower confidence or open an investigation.
|
||||
type Metacognition struct{}
|
||||
|
||||
@@ -103,6 +107,7 @@ func (Metacognition) Observe(ctx context.Context, s *brain.State) ([]brain.Candi
|
||||
}
|
||||
}
|
||||
}
|
||||
volitionChecks(ctx, s, add)
|
||||
// Overconfidence against calibration.
|
||||
if s.Services.Predictions != nil {
|
||||
if cal, err := s.Services.Predictions.Calibrate(ctx, s.Mind.ID, s.Now.Add(-90*24*time.Hour)); err == nil && cal.Overconfident {
|
||||
@@ -160,6 +165,105 @@ func (h Handlers) metacognition(ctx context.Context, c *brain.Cycle, t workspace
|
||||
}
|
||||
}
|
||||
}
|
||||
if check == "intention_outlived_goal" && c.State.Services.Volition != nil {
|
||||
if iid, ok := t.Content["intention_id"].(string); ok {
|
||||
r := recorder(c)
|
||||
r.Cause = ev.ID
|
||||
if _, err := c.State.Services.Volition.Apply(ctx, r, c.State.Mind.ID, ids.ID(iid), volition.Reconsideration{Outcome: volition.Abandon, Reason: "the goal it served is no longer open"}, volition.Trigger{Kind: volition.TriggerGoalCompleted, EventID: ev.ID}); err != nil && !errors.Is(err, volition.ErrClosed) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Runtime.Hold(workspace.Item{Key: "meta:" + check + ":" + ref, Content: t.Summary, Kind: "note", SourceEvent: ev.ID})
|
||||
return nil
|
||||
}
|
||||
|
||||
// volitionChecks audit wanting, intending and acting: strong wants that keep
|
||||
// being suppressed, intentions with no recorded reason, intentions a live
|
||||
// commitment forbids, commitments that cannot be kept, churn, repeated
|
||||
// failed execution, plans whose cost outweighs the intention's value, and
|
||||
// intentions that outlived their goal.
|
||||
func volitionChecks(ctx context.Context, s *brain.State, add func(check, ref, summary string, severity float64, evidence []events.EvidenceRef, extra map[string]any)) {
|
||||
vs := s.Services.Volition
|
||||
if vs == nil {
|
||||
return
|
||||
}
|
||||
if ws, err := vs.Wants(ctx, nil, s.Mind.ID, []volition.WantStatus{volition.WantActive}, 300); err == nil {
|
||||
for _, w := range ws {
|
||||
if w.Suppressions >= 3 && w.Effective(s.Now) >= 0.5 {
|
||||
add("want_suppressed_repeatedly", string(w.ID), fmt.Sprintf("a strong want (%.2f) has been set aside %d times: %s", w.Effective(s.Now), w.Suppressions, truncate(w.Description, 80)), 0.5,
|
||||
[]events.EvidenceRef{{Kind: "want", ID: string(w.ID)}}, map[string]any{"want_id": string(w.ID)})
|
||||
}
|
||||
}
|
||||
}
|
||||
open, err := vs.Open(ctx, nil, s.Mind.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
commitments, _ := vs.Commitments(ctx, nil, s.Mind.ID, []string{"active"}, 500)
|
||||
goalStatus := map[ids.ID]string{}
|
||||
if s.Services.Goals != nil {
|
||||
if gs, err := s.Services.Goals.List(ctx, s.Mind.ID, nil, 500); err == nil {
|
||||
for _, g := range gs {
|
||||
goalStatus[g.ID] = string(g.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, it := range open {
|
||||
ref := []events.EvidenceRef{{Kind: "intention", ID: string(it.ID)}}
|
||||
if len(it.SourceWants) == 0 && it.GoalID.IsZero() && it.CommitmentID.IsZero() && it.ParentID.IsZero() {
|
||||
add("intention_unsupported", string(it.ID), "an intention has no recorded want, goal or commitment behind it: "+truncate(it.Description, 80), 0.6, ref, map[string]any{"intention_id": string(it.ID)})
|
||||
}
|
||||
if a := ActionInfo(ctx, s.Services.Capabilities, s.Mind.ID, it.Proposal); a != nil && !s.Mind.Config.Ablated("commitments") {
|
||||
if chk := volition.CheckAction(commitments, *a, s.Now); len(chk.Forbidden) > 0 {
|
||||
add("intention_conflicts_commitment", string(it.ID), fmt.Sprintf("intention %q conflicts with commitment %q", truncate(it.Description, 60), truncate(chk.Forbidden[0].Description, 60)), 0.7,
|
||||
append(ref, events.EvidenceRef{Kind: "commitment", ID: string(chk.Forbidden[0].ID)}), map[string]any{"intention_id": string(it.ID), "commitment_id": string(chk.Forbidden[0].ID)})
|
||||
}
|
||||
}
|
||||
if st, ok := goalStatus[it.GoalID]; ok && (st == "completed" || st == "abandoned") {
|
||||
add("intention_outlived_goal", string(it.ID), "intention still open although its goal is "+st+": "+truncate(it.Description, 80), 0.5, ref, map[string]any{"intention_id": string(it.ID)})
|
||||
}
|
||||
if !it.PlanID.IsZero() && s.Services.Plans != nil {
|
||||
if p, err := s.Services.Plans.Get(ctx, nil, it.PlanID); err == nil && planning.PlanOpen(p.Status) && it.Priority < 0.4 && (p.Estimate.Approvals >= 3 || p.Estimate.Invocations > p.Budget.MaxInvocations) {
|
||||
add("cost_exceeds_value", string(p.ID), fmt.Sprintf("plan for a low-priority intention (%.2f) needs %d approvals and %d invocations: %s", it.Priority, p.Estimate.Approvals, p.Estimate.Invocations, truncate(it.Description, 60)), 0.4,
|
||||
append(ref, events.EvidenceRef{Kind: "plan", ID: string(p.ID)}), map[string]any{"intention_id": string(it.ID), "plan_id": string(p.ID)})
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.Services.Capabilities != nil {
|
||||
effects := map[string]string{}
|
||||
if inv, err := s.Services.Capabilities.Inventory(ctx, s.Mind.ID); err == nil {
|
||||
for _, i := range inv {
|
||||
effects[i.Capability.Name] = string(i.Effect)
|
||||
if i.Capability.Locked {
|
||||
effects[i.Capability.Name] = "locked"
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, cm := range commitments {
|
||||
if why := volition.Impossible(cm, effects, s.Now); why != "" {
|
||||
add("commitment_impossible", string(cm.ID), "commitment cannot be kept: "+truncate(cm.Description, 60)+" — "+why, 0.7, []events.EvidenceRef{{Kind: "commitment", ID: string(cm.ID)}}, map[string]any{"commitment_id": string(cm.ID)})
|
||||
}
|
||||
}
|
||||
}
|
||||
es, ok := s.Services.Events.(*evstore.Store)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if evs, err := es.Query(ctx, evstore.Filter{MindID: s.Mind.ID, Types: []events.Type{events.IntentionReconsidered, events.IntentionAbandoned, events.IntentionSuperseded, events.IntentionSuspended}, Since: s.Now.Add(-time.Hour), Limit: 200}); err == nil && len(evs) >= 10 {
|
||||
add("intention_churn", "hour:"+s.Now.Format("2006-01-02T15"), fmt.Sprintf("%d intention changes in the last hour", len(evs)), 0.5, nil, map[string]any{"count": len(evs)})
|
||||
}
|
||||
if evs, err := es.Query(ctx, evstore.Filter{MindID: s.Mind.ID, Types: []events.Type{events.PlanStepFailed}, Since: s.Now.Add(-24 * time.Hour), Limit: 300}); err == nil {
|
||||
byAction := map[string]int{}
|
||||
for _, ev := range evs {
|
||||
if capName, _ := ev.Metadata["capability"].(string); capName != "" {
|
||||
byAction[capName]++
|
||||
}
|
||||
}
|
||||
for capName, n := range byAction {
|
||||
if n >= 3 {
|
||||
add("repeated_execution_failure", capName, fmt.Sprintf("%s failed %d times in plans today", capName, n), 0.6, nil, map[string]any{"capability": capName, "count": n})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ type Stores struct {
|
||||
// ablation names the same components everywhere.
|
||||
func NewRegistry(s Stores) *brain.Registry {
|
||||
r := brain.NewRegistry().AddProcess(Attend{}).AddProcess(Retrieve{}).AddProcess(Associate{}).AddProcess(Perceive{}).
|
||||
AddProcess(GoalMonitor{}).AddProcess(Curiosity{}).AddProcess(Metacognition{}).AddProcess(Act{})
|
||||
AddProcess(GoalMonitor{}).AddProcess(Curiosity{}).AddProcess(Metacognition{}).AddProcess(Act{}).AddProcess(Intend{})
|
||||
if s.Motivation != nil {
|
||||
r.AddHook(MotivationHook{Store: s.Motivation})
|
||||
}
|
||||
@@ -40,6 +40,7 @@ func NewRegistry(s Stores) *brain.Registry {
|
||||
if s.Notifications != nil {
|
||||
r.AddHook(NotifyHook{Store: s.Notifications})
|
||||
}
|
||||
r.AddHook(VolitionHook{})
|
||||
Handlers{Memory: s.Memory, Beliefs: s.Beliefs, Conversations: s.Conversations, Graph: s.Graph, Assertions: s.Assertions}.Register(r)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
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"
|
||||
@@ -232,6 +233,14 @@ func (h Handlers) buildIntent(ctx context.Context, c *brain.Cycle, a conversatio
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
package cognition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andydixon/noema/internal/attention"
|
||||
"github.com/andydixon/noema/internal/brain"
|
||||
"github.com/andydixon/noema/internal/capabilities"
|
||||
evstore "github.com/andydixon/noema/internal/events"
|
||||
"github.com/andydixon/noema/internal/planning"
|
||||
"github.com/andydixon/noema/internal/storage"
|
||||
"github.com/andydixon/noema/internal/volition"
|
||||
"github.com/andydixon/noema/internal/workspace"
|
||||
"github.com/andydixon/noema/pkg/events"
|
||||
"github.com/andydixon/noema/pkg/ids"
|
||||
)
|
||||
|
||||
// volitionSource is the event source for volitional changes made inside a
|
||||
// cycle. It deliberately does not start with "brain": a new want or
|
||||
// intention is a change the next cycle should consider.
|
||||
const volitionSource = "volition"
|
||||
|
||||
func recorder(c *brain.Cycle) volition.Recorder {
|
||||
return volition.Recorder{Q: c.Tx, Source: volitionSource, Now: c.State.Now, Correlation: c.State.CycleID,
|
||||
Emit: func(ctx context.Context, _ storage.Querier, n events.New) (events.Event, error) {
|
||||
return c.EmitNow(ctx, n)
|
||||
}}
|
||||
}
|
||||
|
||||
func deriveContext(ctx context.Context, s *brain.State) volition.DeriveContext {
|
||||
d := volition.DeriveContext{Now: s.Now, Drives: map[string]float64{}, Values: s.Mind.Personality.Values, Goals: map[ids.ID]volition.GoalInfo{}, Commitments: map[ids.ID]volition.Commitment{}}
|
||||
for _, dr := range s.Motiv.Drives {
|
||||
d.Drives[dr.Name] = dr.Value
|
||||
}
|
||||
if s.Services.Goals != nil {
|
||||
if gs, err := s.Services.Goals.List(ctx, s.Mind.ID, nil, 300); err == nil {
|
||||
for _, g := range gs {
|
||||
d.Goals[g.ID] = volition.GoalInfo{ID: g.ID, Title: g.Title, Subject: g.Subject, Kind: string(g.Kind), Owner: g.Owner, Priority: g.Priority, DueAt: g.DueAt}
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.Services.Volition != nil {
|
||||
if cs, err := s.Services.Volition.Commitments(ctx, nil, s.Mind.ID, []string{"active"}, 500); err == nil {
|
||||
for _, c := range cs {
|
||||
d.Commitments[c.ID] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// ActionInfo describes a proposed capability action for volition: its
|
||||
// metadata from the registry and the effect policy would give it now.
|
||||
func ActionInfo(ctx context.Context, caps *capabilities.Service, mind ids.ID, p *volition.Proposal) *volition.ActionInfo {
|
||||
if p == nil || p.Kind != "capability" || caps == nil {
|
||||
return nil
|
||||
}
|
||||
a := &volition.ActionInfo{Capability: p.Capability, Target: p.Target, Effect: "unknown"}
|
||||
c, ok := caps.Registry.Get(p.Capability)
|
||||
if !ok {
|
||||
return a
|
||||
}
|
||||
a.Access, a.Risk, a.Idempotent, a.SideEffects, a.WallSeconds = string(c.Access), string(c.Risk), c.Idempotent, c.SideEffects, c.Estimate.WallSeconds
|
||||
if c.Locked {
|
||||
a.Effect = "locked"
|
||||
return a
|
||||
}
|
||||
target := p.Target
|
||||
if c.TargetFrom != nil {
|
||||
target = ""
|
||||
}
|
||||
d, err := caps.Preview(ctx, capabilities.Invocation{MindID: mind, Capability: p.Capability, Target: target, Params: p.Params})
|
||||
if err != nil {
|
||||
a.Effect = "deny"
|
||||
return a
|
||||
}
|
||||
if t, err := c.ResolveTarget("", p.Params); err == nil && t != "" {
|
||||
a.Target = t
|
||||
}
|
||||
a.Effect = string(d.Effect)
|
||||
return a
|
||||
}
|
||||
|
||||
// ---- after-cycle hook: wants and commitments ----
|
||||
|
||||
// VolitionHook derives wants from what the cycle perceived and produced,
|
||||
// satisfies answer wants when a reply was sent, raises due, overdue,
|
||||
// expired and conflicting commitments, and lets faded wants expire.
|
||||
type VolitionHook struct{}
|
||||
|
||||
// Name implements brain.Hook.
|
||||
func (VolitionHook) Name() string { return "volition" }
|
||||
|
||||
// AfterCycle implements brain.Hook.
|
||||
func (VolitionHook) AfterCycle(ctx context.Context, c *brain.Cycle, emitted []events.Event) error {
|
||||
vs := c.State.Services.Volition
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
r := recorder(c)
|
||||
mind := c.State.Mind.ID
|
||||
dctx := deriveContext(ctx, c.State)
|
||||
all := append(append([]events.Event{}, c.State.NewEvents...), emitted...)
|
||||
for i, ev := range all {
|
||||
if ev.Source == volitionSource && ev.Category == events.CatVolition {
|
||||
continue // volition's own records never derive further wants
|
||||
}
|
||||
if i < len(c.State.NewEvents) && strings.HasPrefix(ev.Source, "brain") {
|
||||
continue // produced by an earlier cycle, whose hook already saw it
|
||||
}
|
||||
for _, cand := range volition.Derive(ev, dctx) {
|
||||
if _, _, err := vs.UpsertWant(ctx, r, mind, cand); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, ev := range emitted {
|
||||
if ev.Type != events.ResponseGenerated {
|
||||
continue
|
||||
}
|
||||
msg, _ := ev.Metadata["message_id"].(string)
|
||||
if msg == "" {
|
||||
continue
|
||||
}
|
||||
if ws, err := vs.Wants(ctx, c.Tx, mind, []volition.WantStatus{volition.WantActive}, 500); err == nil {
|
||||
for _, w := range ws {
|
||||
if w.Subject == "answer:message:"+msg {
|
||||
if err := vs.ResolveWant(ctx, r, mind, w.ID, volition.WantSatisfied, "replied (event "+string(ev.ID)+")"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, cm := range dctx.Commitments {
|
||||
switch volition.Assess(cm, c.State.Now) {
|
||||
case volition.StateDue:
|
||||
if err := vs.MarkDue(ctx, r, cm); err != nil {
|
||||
return err
|
||||
}
|
||||
due := events.Event{Type: events.CommitmentDue, Metadata: map[string]any{"commitment_id": string(cm.ID)}}
|
||||
for _, cand := range volition.Derive(due, dctx) {
|
||||
if _, _, err := vs.UpsertWant(ctx, r, mind, cand); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case volition.StateOverdue:
|
||||
if _, err := vs.CloseCommitment(ctx, r, mind, cm.ID, "broken", "the deadline "+cm.DueAt.Format(time.RFC3339)+" passed without fulfilment", false); err != nil && !errors.Is(err, volition.ErrInvalid) {
|
||||
return err
|
||||
}
|
||||
case volition.StateExpired:
|
||||
if _, err := vs.CloseCommitment(ctx, r, mind, cm.ID, "expired", "its expiry passed", false); err != nil && !errors.Is(err, volition.ErrInvalid) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
var live []volition.Commitment
|
||||
for _, cm := range dctx.Commitments {
|
||||
live = append(live, cm)
|
||||
}
|
||||
for _, cf := range volition.CommitmentConflicts(live, c.State.Now) {
|
||||
key := pairKey(cf.A.ID, cf.B.ID)
|
||||
if recentlyRaised(ctx, c, events.CommitmentConflict, "pair", key) {
|
||||
continue
|
||||
}
|
||||
if _, err := c.EmitNow(ctx, events.New{Type: events.CommitmentConflict, Source: volitionSource, Summary: fmt.Sprintf("commitments conflict: %q and %q — %s", cf.A.Description, cf.B.Description, cf.Reason), Salience: 0.7, Confidence: 1,
|
||||
Evidence: []events.EvidenceRef{{Kind: "commitment", ID: string(cf.A.ID)}, {Kind: "commitment", ID: string(cf.B.ID)}}, Metadata: map[string]any{"pair": key, "a": string(cf.A.ID), "b": string(cf.B.ID)},
|
||||
Payload: map[string]any{"reason": cf.Reason, "a_weight": cf.A.Weight(), "b_weight": cf.B.Weight()}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := vs.ExpireWants(ctx, r, mind, 40); err != nil {
|
||||
return err
|
||||
}
|
||||
return vs.SettleConflicts(ctx, c.Tx, mind, c.State.Now)
|
||||
}
|
||||
|
||||
func pairKey(a, b ids.ID) string {
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
return string(a) + "|" + string(b)
|
||||
}
|
||||
|
||||
func recentlyRaised(ctx context.Context, c *brain.Cycle, typ events.Type, key, value string) bool {
|
||||
es, ok := c.State.Services.Events.(*evstore.Store)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
evs, err := es.Query(ctx, evstore.Filter{MindID: c.State.Mind.ID, Types: []events.Type{typ}, Since: c.State.Now.Add(-24 * time.Hour), Limit: 200})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, ev := range evs {
|
||||
if ev.Metadata[key] == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---- intention formation ----
|
||||
|
||||
// Intend proposes deciding about the strongest active wants that have not
|
||||
// been decided since they were last reinforced. Whether the decision is
|
||||
// made at all depends on attention: the candidate competes for the
|
||||
// workspace like any other thought.
|
||||
type Intend struct{}
|
||||
|
||||
// Name implements brain.Process.
|
||||
func (Intend) Name() string { return "intend" }
|
||||
|
||||
// Observe implements brain.Process.
|
||||
func (Intend) Observe(ctx context.Context, s *brain.State) ([]brain.Candidate, error) {
|
||||
vs := s.Services.Volition
|
||||
if vs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
ws, err := vs.Wants(ctx, nil, s.Mind.ID, []volition.WantStatus{volition.WantActive}, 200)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decided := map[ids.ID]time.Time{}
|
||||
if ds, err := vs.Decisions(ctx, s.Mind.ID, volition.DecisionFilter{Limit: 500}); err == nil {
|
||||
for _, d := range ds {
|
||||
if (d.Kind == "formation" || d.Kind == "non_formation") && !d.WantID.IsZero() && d.CreatedAt.After(decided[d.WantID]) {
|
||||
decided[d.WantID] = d.CreatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
volition.SortWants(ws, s.Now)
|
||||
var out []brain.Candidate
|
||||
for _, w := range ws {
|
||||
if len(out) >= 3 {
|
||||
break
|
||||
}
|
||||
if w.Valence == "avoid" || (w.Origin == volition.OriginRequest && w.Proposal == nil) {
|
||||
continue // values constrain other wants; replies are handled by perception
|
||||
}
|
||||
if at, ok := decided[w.ID]; ok && !w.LastReinforcedAt.After(at) {
|
||||
continue // already decided; only new reinforcement reopens the question
|
||||
}
|
||||
eff := w.Effective(s.Now)
|
||||
prio := 0.4
|
||||
if w.Origin == volition.OriginGoal || w.Origin == volition.OriginOperator || w.Origin == volition.OriginCommitment {
|
||||
prio = 0.9
|
||||
} else if w.Origin == volition.OriginRequest {
|
||||
prio = 0.7
|
||||
}
|
||||
var goalRel float64
|
||||
for _, g := range s.Goals {
|
||||
if g.ID == w.GoalID {
|
||||
goalRel = g.Priority
|
||||
}
|
||||
}
|
||||
out = append(out, brain.Candidate{Kind: "intend", Subject: "intend:" + string(w.ID), Summary: fmt.Sprintf("decide whether to pursue: %s (%.2f)", w.Description, eff),
|
||||
Content: map[string]any{"want_id": string(w.ID)},
|
||||
Features: attention.Features{Salience: eff, Urgency: w.Urgency, GoalRelevance: goalRel, Consequence: 0.5, Confidence: 0.8, OperatorPriority: prio, Novelty: 0.3},
|
||||
Confidence: 0.8, Evidence: []events.EvidenceRef{{Kind: "want", ID: string(w.ID)}}, SourceEvents: nonZero(w.OriginEvent)})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func nonZero(id ids.ID) []ids.ID {
|
||||
if id.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return []ids.ID{id}
|
||||
}
|
||||
|
||||
// intend runs deterministic formation for an admitted want and records the
|
||||
// decision, whatever it is. A formed intention is planned in the same
|
||||
// transaction; nothing acts until the executor picks the plan up after commit.
|
||||
func (h Handlers) intend(ctx context.Context, c *brain.Cycle, t workspace.Thought) error {
|
||||
s := c.State
|
||||
vs := s.Services.Volition
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
wid, _ := t.Content["want_id"].(string)
|
||||
w, err := vs.GetWant(ctx, c.Tx, s.Mind.ID, ids.ID(wid))
|
||||
if err != nil || w.Status != volition.WantActive {
|
||||
return nil
|
||||
}
|
||||
r := recorder(c)
|
||||
r.Cause = t.EventID
|
||||
in := volition.FormationInput{Want: w, Now: s.Now, Values: s.Mind.Personality.Values, Confidence: 0.6, IgnoreCommitments: s.Mind.Config.Ablated("commitments"),
|
||||
Operator: w.Origin == volition.OriginOperator}
|
||||
if in.Commitments, err = vs.Commitments(ctx, c.Tx, s.Mind.ID, []string{"active"}, 500); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Active, err = vs.Open(ctx, c.Tx, s.Mind.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if !w.GoalID.IsZero() && s.Services.Goals != nil {
|
||||
if g, err := s.Services.Goals.Get(ctx, s.Mind.ID, w.GoalID); err == nil {
|
||||
in.Goal = &volition.GoalInfo{ID: g.ID, Title: g.Title, Priority: g.Priority, Owner: g.Owner}
|
||||
in.Operator = in.Operator || g.Owner == "operator"
|
||||
}
|
||||
}
|
||||
if !w.CommitmentID.IsZero() {
|
||||
if cm, err := vs.GetCommitment(ctx, c.Tx, s.Mind.ID, w.CommitmentID); err == nil {
|
||||
in.CommitmentWeight = cm.Weight()
|
||||
}
|
||||
}
|
||||
in.Action = ActionInfo(ctx, s.Services.Capabilities, s.Mind.ID, w.Proposal)
|
||||
if s.Services.Capabilities != nil {
|
||||
if inv, err := s.Services.Capabilities.Inventory(ctx, s.Mind.ID); err == nil {
|
||||
for _, i := range inv {
|
||||
if i.Capability.Access == capabilities.AccessRead && !i.Capability.Locked && (i.Effect == capabilities.Allow || i.Effect == capabilities.Ask) && i.Capability.Name != "noema.note" {
|
||||
in.ReadCapabilities++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Value-derived tensions are made explicit before deciding.
|
||||
if in.Action != nil {
|
||||
for _, cand := range volition.Companions(w, *in.Action, in.Values) {
|
||||
avoid, _, err := vs.UpsertWant(ctx, r, s.Mind.ID, cand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res := volition.ResolveConflict(w, avoid, s.Now, nil, map[ids.ID]float64{w.ID: in.CommitmentWeight})
|
||||
if _, _, err := vs.RecordConflict(ctx, r, w, avoid, "value", res); err != nil {
|
||||
return err
|
||||
}
|
||||
in.Constrained = in.Constrained || (res.Preferred == w.ID && res.Constrain)
|
||||
in.Deferred = in.Deferred || res.Preferred != w.ID
|
||||
}
|
||||
}
|
||||
f := volition.Form(in)
|
||||
inputs := map[string]any{"want": w.Description, "effective_strength": w.Effective(s.Now), "action": in.Action, "commitments_considered": len(in.Commitments), "open_intentions": len(in.Active), "read_capabilities": in.ReadCapabilities, "applied": f.Applied}
|
||||
if f.Outcome != volition.Formed {
|
||||
if f.Reason == volition.ReasonDuplicate {
|
||||
return nil
|
||||
}
|
||||
evID, err := c.EmitNow(ctx, events.New{Type: events.Metacognition, Source: volitionSource, Summary: "did not form an intention for \"" + w.Description + "\": " + f.Rationale, Salience: 0.3 + 0.3*w.Effective(s.Now), Confidence: 1,
|
||||
CausationID: t.EventID, Evidence: []events.EvidenceRef{{Kind: "want", ID: string(w.ID)}}, Metadata: map[string]any{"check": "intention_not_formed", "ref": string(w.ID), "reason": f.Reason, "want_id": string(w.ID)}, Payload: f})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := vs.RecordDecision(ctx, c.Tx, volition.Decision{MindID: s.Mind.ID, Kind: "non_formation", Outcome: f.Outcome, Reason: f.Reason, Rationale: f.Rationale, WantID: w.ID, Terms: f.Terms, Inputs: inputs, EventID: evID.ID, CreatedAt: s.Now}); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.Suppress {
|
||||
if err := vs.SuppressWant(ctx, r, w, f.Rationale, f.Applied); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.Runtime.Hold(workspace.Item{Key: "want:" + string(w.ID), Content: "not pursuing: " + w.Description + " (" + f.Reason + ")", Kind: "note", Ref: string(w.ID), SourceEvent: evID.ID})
|
||||
return nil
|
||||
}
|
||||
if f.Displace != "" {
|
||||
if _, err := vs.Apply(ctx, r, s.Mind.ID, f.Displace, volition.Reconsideration{Outcome: volition.Suspend, Reason: "capacity: a higher-priority intention formed (" + w.Description + ")"}, volition.Trigger{Kind: volition.TriggerSuperior, Detail: w.Description}); err != nil && !errors.Is(err, volition.ErrClosed) {
|
||||
return err
|
||||
}
|
||||
if displaced, err := vs.GetIntention(ctx, c.Tx, s.Mind.ID, f.Displace); err == nil && len(displaced.SourceWants) > 0 {
|
||||
if other, err := vs.GetWant(ctx, c.Tx, s.Mind.ID, displaced.SourceWants[0]); err == nil {
|
||||
res := volition.ResolveConflict(w, other, s.Now, nil, map[ids.ID]float64{w.ID: in.CommitmentWeight})
|
||||
vs.RecordConflict(ctx, r, w, other, "priority", res) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
}
|
||||
var required []string
|
||||
if w.Proposal != nil && w.Proposal.Capability != "" {
|
||||
required = []string{w.Proposal.Capability}
|
||||
}
|
||||
expected := w.Description
|
||||
if in.Action != nil {
|
||||
expected = "the effect of " + in.Action.Capability + " on " + in.Action.Target + " is observed"
|
||||
}
|
||||
terms := map[string]any{"score": f.Score, "terms": f.Terms, "rationale": f.Rationale, "applied_commitments": f.Applied, "constrained": in.Constrained}
|
||||
it, err := vs.CreateIntention(ctx, r, volition.Intention{MindID: s.Mind.ID, Description: f.Description, DesiredOutcome: f.DesiredOutcome, Kind: f.Kind, SourceWants: []ids.ID{w.ID}, GoalID: w.GoalID, CommitmentID: w.CommitmentID,
|
||||
ExpectedResult: expected, Confidence: in.Confidence, Priority: f.Priority, RequireApproval: f.RequireApproval, Proposal: w.Proposal, RequiredCapabilities: required, Formation: terms})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := vs.RecordDecision(ctx, c.Tx, volition.Decision{MindID: s.Mind.ID, Kind: "formation", Outcome: string(f.Kind), Reason: f.Reason, Rationale: f.Rationale, WantID: w.ID, IntentionID: it.ID, Terms: f.Terms, Inputs: inputs, EventID: it.FormedEvent, CreatedAt: s.Now}); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Runtime.Hold(workspace.Item{Key: "intention:" + string(it.ID), Content: "intends to " + it.Description, Kind: "thought", Ref: string(it.ID), SourceEvent: it.FormedEvent})
|
||||
if s.Services.Executor == nil || it.Kind == volition.IntentRespond {
|
||||
return nil
|
||||
}
|
||||
if s.Mode == brain.ModeDreaming {
|
||||
return nil // dreams may intend; plans are made awake
|
||||
}
|
||||
if _, err := s.Services.Executor.Start(ctx, r, it); err != nil {
|
||||
reason := "no possible plan: " + err.Error()
|
||||
outcome := volition.Abandon
|
||||
if errors.Is(err, planning.ErrInvalidPlan) {
|
||||
outcome, reason = volition.Block, "the proposed plan failed validation: "+err.Error()
|
||||
}
|
||||
if _, aerr := vs.Apply(ctx, r, s.Mind.ID, it.ID, volition.Reconsideration{Outcome: outcome, BlockedKind: volition.BlockPrereq, Reason: reason}, volition.Trigger{Kind: "planning", Detail: err.Error()}); aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
if outcome == volition.Abandon {
|
||||
return vs.SuppressWant(ctx, r, w, reason, nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// want records an explicit action request as a want with its proposed means.
|
||||
func (h Handlers) want(ctx context.Context, c *brain.Cycle, t workspace.Thought) error {
|
||||
vs := c.State.Services.Volition
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
src, _ := t.Content["source_event"].(string)
|
||||
actor, _ := t.Content["actor"].(string)
|
||||
text, _ := t.Content["text"].(string)
|
||||
operator, _ := t.Content["operator"].(bool)
|
||||
p, ok := t.Content["proposal"].(volition.Proposal)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
ev := events.Event{ID: ids.ID(src)}
|
||||
r := recorder(c)
|
||||
r.Cause = t.EventID
|
||||
for _, cand := range volition.FromRequest(ev, actor, text, p, deriveContext(ctx, c.State), operator) {
|
||||
w, _, err := vs.UpsertWant(ctx, r, c.State.Mind.ID, cand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Runtime.Hold(workspace.Item{Key: "want:" + string(w.ID), Content: "wants: " + w.Description, Kind: "thought", Ref: string(w.ID), SourceEvent: ids.ID(src)})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user