Files
noema/internal/cognition/goals.go
T
Andy Dixon 7d67745ff8 test(agency): the Part L incident scenario end to end, and failure modes
The full incident scenario now runs against the assembled daemon: an
operator goal becomes a want to reduce uncertainty, an investigation
intention and a diagnosis plan; telemetry is read; logs need authority,
so a scoped request with risk, mitigation and fallback is raised and the
intention is blocked until an operator grants it; the logs are read with
personal data masked and summarised back into cognition; the operator
supplies the judgement that a restart should help; a standing commitment
turns the restart into a request for approval; after approval the typed
capability runs and health is verified rather than assumed; the fault
persists, so a prediction error weakens the supporting belief, the plan
is replanned into an escalation that reuses the live log grant and does
not repeat the restart; once the database is fixed a second approved
restart verifies, the procedure is learnt, and "Why did you restart
atlas-worker?" is answered from the recorded chain and scores as
supported.

Changes the scenario exposed:
- New operator goals are raised by the goal monitor, so a cycle runs and
  wants are derived from them.
- Intentions link live beliefs about their action's target, so a failed
  expectation is evidence against the belief it rested on and the plan
  lists it as an assumption.
- Reconsideration reacts to a plan's own intention only; a subordinate
  "obtain approval" intention shares the plan and previously caused the
  parent's plan to be closed.
- The harness accepts a clock for the executor's verification windows.

Further failure-mode tests: a retracted assumption replans a waiting plan
away from the restart and withdraws its approval request; conflicting
commitments are raised once in a cycle while both stay represented.
2026-09-27 23:34:43 +01:00

112 lines
5.6 KiB
Go

package cognition
import (
"context"
"strings"
"time"
"github.com/andydixon/noema/internal/attention"
"github.com/andydixon/noema/internal/brain"
"github.com/andydixon/noema/internal/goals"
"github.com/andydixon/noema/internal/workspace"
"github.com/andydixon/noema/pkg/events"
"github.com/andydixon/noema/pkg/ids"
)
// GoalMonitor watches the goal tree: stalled goals are raised for attention,
// and goals whose subject appears in new events get a progress nudge
// candidate. It never completes a goal on its own; that needs evidence or
// an operator.
type GoalMonitor struct {
StallAfter time.Duration
}
// Name implements brain.Process.
func (GoalMonitor) Name() string { return "goal-monitor" }
// Observe implements brain.Process.
func (g GoalMonitor) Observe(ctx context.Context, s *brain.State) ([]brain.Candidate, error) {
if s.Services.Goals == nil {
return nil, nil
}
stall := g.StallAfter
if stall <= 0 {
stall = 24 * time.Hour
}
var out []brain.Candidate
stalled, _ := s.Services.Goals.Stalled(ctx, s.Mind.ID, stall)
for i, gl := range stalled {
if i >= 3 {
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), "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)}}})
}
// A goal an operator just set deserves attention of its own.
for _, ev := range s.NewEvents {
if ev.Type != events.GoalCreated || ev.Source != "operator" {
continue
}
gid, _ := ev.Metadata["goal_id"].(string)
for _, gl := range s.Goals {
if string(gl.ID) == gid {
out = append(out, brain.Candidate{Kind: "goal", Subject: string(gl.ID) + ":new", Summary: "new goal from an operator: " + gl.Title,
Content: map[string]any{"goal_id": gid, "reason": "set by an operator", "ref": gid, "source_event": string(ev.ID)},
Features: attention.Features{Salience: 0.7, GoalRelevance: gl.Priority, Consequence: 0.6, Urgency: 0.5, Confidence: 1, OperatorPriority: 1}, Confidence: 1,
SourceEvents: []ids.ID{ev.ID}, Evidence: []events.EvidenceRef{{Kind: "goal", ID: gid}, {Kind: "event", ID: string(ev.ID)}}})
}
}
}
// New external events touching a goal's subject.
for _, ev := range s.NewEvents {
if strings.HasPrefix(ev.Source, "brain") || ev.Type != events.Observation {
continue
}
if rel, id := goals.Relevance(ev.Summary, s.Goals); rel >= 0.5 && !id.IsZero() {
out = append(out, brain.Candidate{Kind: "goal", Subject: string(id) + ":" + string(ev.ID), Summary: "evidence for a goal: " + truncate(ev.Summary, 100),
Content: map[string]any{"goal_id": string(id), "reason": "new observation relevant to the goal", "source_event": string(ev.ID), "relevance": rel, "ref": string(id)},
Features: attention.Features{Salience: ev.Salience * 0.7, GoalRelevance: rel, Consequence: 0.4, Confidence: 1}, Confidence: 1, SourceEvents: []ids.ID{ev.ID},
Evidence: []events.EvidenceRef{{Kind: "goal", ID: string(id)}, {Kind: "event", ID: string(ev.ID)}}})
}
}
return out, nil
}
// goal handles admitted goal thoughts: holds the goal in working memory and
// records that it was attended (progress nudges are logged, not invented).
func (h Handlers) goal(ctx context.Context, c *brain.Cycle, t workspace.Thought) error {
gid, _ := t.Content["goal_id"].(string)
reason, _ := t.Content["reason"].(string)
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, "stalled": t.Content["stalled"] == true}, Payload: map[string]any{"goal_id": gid, "reason": reason}})
return nil
}
// question handles admitted curiosity questions: an investigation goal is
// created (once per subject) so the question persists beyond the workspace.
func (h Handlers) question(ctx context.Context, c *brain.Cycle, t workspace.Thought) error {
if c.State.Services.Goals == nil {
return nil
}
subject, _ := t.Content["subject"].(string)
question, _ := t.Content["question"].(string)
src, _ := t.Content["source_event"].(string)
if existing, err := c.State.Services.Goals.FindBySubject(ctx, c.Tx, c.State.Mind.ID, goals.KindInvestigation, subject); err == nil {
c.Runtime.Hold(workspace.Item{Key: "goal:" + string(existing.ID), Content: existing.Title, Kind: "goal", Ref: string(existing.ID)})
return nil
}
prio := 0.3 + 0.4*c.State.Motiv.Affect.Curiosity
g, err := c.State.Services.Goals.Create(ctx, c.Tx, goals.New{MindID: c.State.Mind.ID, Title: question, Kind: goals.KindInvestigation, Priority: prio, Subject: subject, Owner: "mind", OriginEvent: t.EventID})
if err != nil {
return err
}
c.Emit(events.New{Type: events.GoalCreated, Source: "brain.curiosity", Summary: "curious: " + question, Salience: 0.4 + 0.3*prio, Confidence: 0.6, CausationID: t.EventID, ParentIDs: []ids.ID{t.EventID, ids.ID(src)},
Evidence: append(t.Evidence, events.EvidenceRef{Kind: "goal", ID: string(g.ID)}), Metadata: map[string]any{"goal_id": string(g.ID), "kind": g.Kind, "priority": prio}, Payload: g})
c.Runtime.Hold(workspace.Item{Key: "goal:" + string(g.ID), Content: "investigating: " + question, Kind: "goal", Ref: string(g.ID), SourceEvent: ids.ID(src)})
return nil
}