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.
127 lines
4.1 KiB
Go
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
|
|
}
|