diff --git a/internal/planning/executor.go b/internal/planning/executor.go index 4a3ad46..3db0823 100644 --- a/internal/planning/executor.go +++ b/internal/planning/executor.go @@ -372,7 +372,7 @@ func (e *Executor) explain(p Plan, it volition.Intention, st Step, c capabilitie } var alts []string for _, a := range p.Alternatives { - if !a.Selected && a.Why != "already failed" { + if !a.Selected && a.Why != "already failed" && !strings.HasPrefix(a.Why, "not consulted") { alts = append(alts, a.Strategy) } } diff --git a/internal/planning/executor_test.go b/internal/planning/executor_test.go index 12861d0..95cadd6 100644 --- a/internal/planning/executor_test.go +++ b/internal/planning/executor_test.go @@ -510,6 +510,40 @@ func TestInvalidModelPlanNeverRuns(t *testing.T) { } } +// A model is consulted only when no deterministic strategy applies (B2): +// planning a direct capability action must not spend a live model call on +// an alternative that cannot be selected. +func TestModelIsNotConsultedWhenADeterministicStrategyApplies(t *testing.T) { + f := newFixture(t) + calls := 0 + f.exec.Planner.Model = func(context.Context, ids.ID, string, []capabilities.Capability) (string, ids.ID, error) { + calls++ + return `{"steps":[]}`, ids.Nil, nil + } + it, _ := f.vol.CreateIntention(f.ctx, f.vol.Direct(f.es, "test"), volition.Intention{MindID: f.mind.ID, Description: "check the status page", Kind: volition.IntentAct, + Proposal: &volition.Proposal{Kind: "capability", Capability: "service.status", Params: map[string]any{"service": "atlas-worker"}}}) + p, err := f.exec.Start(f.ctx, f.exec.rec(), it) + if err != nil { + t.Fatal(err) + } + if calls != 0 { + t.Fatalf("the model was called %d time(s) although the direct strategy applied", calls) + } + recorded := false + for _, a := range p.Alternatives { + recorded = recorded || (a.Strategy == "model" && !a.Selected && strings.HasPrefix(a.Why, "not consulted")) + } + if p.Strategy != "direct" || !recorded { + t.Fatalf("expected the direct strategy with the model recorded as not consulted: %+v", p.Alternatives) + } + // With no deterministic strategy the model is consulted. + pursue, _ := f.vol.CreateIntention(f.ctx, f.vol.Direct(f.es, "test"), volition.Intention{MindID: f.mind.ID, Description: "make the dashboard faster", Kind: volition.IntentPursue}) + f.exec.Start(f.ctx, f.exec.rec(), pursue) //nolint:errcheck + if calls != 1 { + t.Fatalf("the model should be consulted once when nothing else applies, got %d", calls) + } +} + // Regression: an executor holding a stale "pending" copy of a step must not // move it back to "ready" after another executor has claimed it, or the // step can be claimed and run a second time. diff --git a/internal/planning/planner.go b/internal/planning/planner.go index e1e1ad1..8f8df47 100644 --- a/internal/planning/planner.go +++ b/internal/planning/planner.go @@ -78,6 +78,13 @@ func (p *Planner) Propose(ctx context.Context, it volition.Intention, previous [ alts = append(alts, Alternative{Strategy: s.name, Description: "tried in an earlier revision", Why: "already failed"}) continue } + if s.name == "model" && chosen != nil { + // A model is consulted only when no deterministic strategy + // applies; building it here would spend a live call on an + // alternative that cannot be selected. + alts = append(alts, Alternative{Strategy: s.name, Description: "model decomposition", ExpectedConfidence: s.confidence, Why: "not consulted: a deterministic strategy applies"}) + continue + } d, err := s.build() if err != nil { alts = append(alts, Alternative{Strategy: s.name, Description: err.Error(), Why: "not buildable"})