Files
noema/internal/planning/react.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

127 lines
4.1 KiB
Go

package planning
import (
"context"
"errors"
"strings"
"github.com/andydixon/noema/internal/capabilities"
"github.com/andydixon/noema/internal/volition"
"github.com/andydixon/noema/pkg/events"
"github.com/andydixon/noema/pkg/ids"
)
// React reconsiders open intentions and plans when something they depend on
// changes: a new commitment, a weakened or retracted assumption, a goal
// completed or abandoned. It is deterministic, runs outside cognitive
// cycles (from the event bus), and never acts; replanning may follow.
func (e *Executor) React(ctx context.Context, ev events.Event) error {
switch ev.Type {
case events.CommitmentCreated, events.BeliefUpdated, events.BeliefRetracted, events.BeliefChallenged, events.GoalCompleted, events.GoalAbandoned:
default:
return nil
}
m, err := e.Minds.Get(ctx, ev.MindID)
if err != nil || m.Status != "active" {
return err
}
open, err := e.Volition.Open(ctx, nil, ev.MindID)
if err != nil {
return err
}
meta := func(k string) string { s, _ := ev.Metadata[k].(string); return s }
for _, it := range open {
if !it.ParentID.IsZero() {
continue // a subordinate intention shares its parent's plan; the parent is reconsidered
}
var t *volition.Trigger
switch ev.Type {
case events.CommitmentCreated:
if m.Config.Ablated("commitments") {
return nil
}
cm, err := e.Volition.GetCommitment(ctx, nil, ev.MindID, ids.ID(meta("commitment_id")))
if err != nil || it.Proposal == nil || it.Proposal.Kind != "capability" {
continue
}
a := e.actionInfo(ctx, it)
t = &volition.Trigger{Kind: volition.TriggerCommitmentChanged, EventID: ev.ID, Commitment: &cm, Action: a, Detail: cm.Description}
case events.GoalCompleted, events.GoalAbandoned:
if string(it.GoalID) != meta("goal_id") || it.GoalID.IsZero() {
continue
}
kind := volition.TriggerGoalCompleted
if ev.Type == events.GoalAbandoned {
kind = volition.TriggerGoalAbandoned
}
t = &volition.Trigger{Kind: kind, EventID: ev.ID, Detail: ev.Summary}
default: // belief changes: only plans that assume the belief care
bid := meta("belief_id")
if bid == "" || it.PlanID.IsZero() || e.Resolver == nil || e.Resolver.Beliefs == nil {
continue
}
p, err := e.Plans.Get(ctx, nil, it.PlanID)
if err != nil || !PlanOpen(p.Status) {
continue
}
for _, a := range p.Assumptions {
if string(a.BeliefID) != bid {
continue
}
b, err := e.Resolver.Beliefs.Get(ctx, nil, ev.MindID, a.BeliefID)
if err != nil {
continue
}
if b.Confidence < a.MinConfidence || b.Status == "rejected" || b.Status == "superseded" {
t = &volition.Trigger{Kind: volition.TriggerBeliefWeakened, EventID: ev.ID, Detail: "an assumption no longer holds: \"" + b.Proposition + "\" is " + string(b.Status)}
}
}
}
if t == nil {
continue
}
if err := e.apply(ctx, it, *t); err != nil && !errors.Is(err, volition.ErrClosed) {
return err
}
}
return nil
}
func (e *Executor) apply(ctx context.Context, it volition.Intention, t volition.Trigger) error {
if !it.PlanID.IsZero() {
if p, err := e.Plans.Get(ctx, nil, it.PlanID); err == nil && PlanOpen(p.Status) && p.IntentionID == it.ID {
return e.reconsider(ctx, p, it, t)
}
}
rc := volition.Reconsider(it, t, volition.ReconsiderContext{Now: e.now()})
if rc.Outcome == volition.Keep {
return nil
}
_, err := e.Volition.Apply(ctx, e.rec(), it.MindID, it.ID, rc, t)
return err
}
func (e *Executor) actionInfo(ctx context.Context, it volition.Intention) *volition.ActionInfo {
p := it.Proposal
a := &volition.ActionInfo{Capability: p.Capability, Target: p.Target, Effect: "unknown"}
c, ok := e.Caps.Registry.Get(p.Capability)
if !ok {
return a
}
a.Access, a.Risk, a.Idempotent, a.SideEffects = string(c.Access), string(c.Risk), c.Idempotent, c.SideEffects
if t, err := c.ResolveTarget("", p.Params); err == nil && t != "" {
a.Target = t
}
if c.Locked {
a.Effect = "locked"
return a
}
d, err := e.Caps.Preview(ctx, capabilities.Invocation{MindID: it.MindID, Capability: p.Capability, Params: p.Params})
if err != nil {
a.Effect = "deny"
return a
}
a.Effect = strings.ToLower(string(d.Effect))
return a
}