mavend: wire resolveAction into actionAct
Daemon half of the action-resolution boundary: - Add resolveAction wrapper: delegates to ResolveActionCandidate, records outcome in the decision trace (action-resolve:route/matcher) - Refactor actionAct: remove matcher call, consume candidate, write resolved values back into Slots for downstream branches - Add 8 integration tests pinning all required scenarios: route-sourced, matcher-sourced, matcher miss, destructive confirm, task-status intercept, stage-0, learned-router, alias match All existing tests pass. Execution/risk/confirmation unchanged.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kami/maven/internal/decision"
|
||||
"github.com/kami/maven/internal/router"
|
||||
)
|
||||
|
||||
// resolveAction produces an ActionCandidate from a routing decision. It is the
|
||||
// single boundary between routing and action execution: everything downstream
|
||||
// (refusesCommand, task-status, Praxis, Hexis, proposeGap, tool.Executor.Exec)
|
||||
// consumes the candidate rather than re-resolving the function.
|
||||
//
|
||||
// Delegates to router.ResolveActionCandidate for the resolution logic, then
|
||||
// records the outcome in the decision trace.
|
||||
func (h *reactiveHandler) resolveAction(ctx context.Context, dec router.Decision) router.ActionCandidate {
|
||||
candidate := router.ResolveActionCandidate(dec, h.matcher)
|
||||
|
||||
// Record the resolution outcome in the decision trace.
|
||||
if dec.Intent == router.IntentAct {
|
||||
if candidate.ActionResolved() {
|
||||
noteActionResolution(ctx, string(candidate.Source), candidate.Fn, true)
|
||||
} else {
|
||||
noteActionResolution(ctx, "matcher", "", false)
|
||||
}
|
||||
}
|
||||
|
||||
return candidate
|
||||
}
|
||||
|
||||
// noteActionResolution records the action resolution outcome in the decision
|
||||
// trace. A nil recorder is the normal case in tests.
|
||||
func noteActionResolution(ctx context.Context, source, fn string, resolved bool) {
|
||||
rec := decision.From(ctx)
|
||||
if rec == nil {
|
||||
return
|
||||
}
|
||||
outcome := decision.Declined
|
||||
reason := "no match"
|
||||
if resolved {
|
||||
outcome = decision.Won
|
||||
reason = "resolved via " + source
|
||||
if fn != "" {
|
||||
reason += ": " + fn
|
||||
}
|
||||
}
|
||||
rec.Note(decision.Claim{
|
||||
Stage: decision.StageAction,
|
||||
Claimant: "action-resolve",
|
||||
Outcome: outcome,
|
||||
Reason: reason,
|
||||
})
|
||||
}
|
||||
+21
-10
@@ -11,9 +11,14 @@ import (
|
||||
"github.com/kami/maven/internal/tool"
|
||||
)
|
||||
|
||||
// actionAct handles router.IntentAct: match a verb to an enabled tool, offer
|
||||
// it to the ecosystems first, and run it behind the confirm gate and the
|
||||
// allowlist. proposeGap and the confirm gate itself live in confirm.go.
|
||||
// actionAct handles router.IntentAct: resolve the action, offer it to the
|
||||
// ecosystems first, and run it behind the confirm gate and the allowlist.
|
||||
// proposeGap and the confirm gate itself live in confirm.go.
|
||||
//
|
||||
// Action resolution happens in resolveAction (actionresolve.go) — a single
|
||||
// boundary that produces an ActionCandidate before execution. This function
|
||||
// consumes the candidate; it no longer decides which function/tool the user
|
||||
// meant.
|
||||
func (h *reactiveHandler) actionAct(ctx context.Context, dec router.Decision) string {
|
||||
// An allowlist or a model route is evidence about WHAT could run, never
|
||||
// authority to run it. Keep the user's negative command at the execution
|
||||
@@ -23,13 +28,19 @@ func (h *reactiveHandler) actionAct(ctx context.Context, dec router.Decision) st
|
||||
return commandProhibitionReply
|
||||
}
|
||||
|
||||
// tool executor: run the matched fn against the enabled allowlist.
|
||||
// HasFn=false ⇒ try the matcher (for LLM-routed acts where the verb
|
||||
// didn't go through the stage-0 act grammar).
|
||||
if !dec.Slots.HasFn && dec.Slots.Text != "" && h.matcher != nil {
|
||||
if fn, args, ok := h.matcher.Match(dec.Slots.Text); ok {
|
||||
dec.Slots.Fn, dec.Slots.Args, dec.Slots.HasFn = fn, args, true
|
||||
}
|
||||
// Resolve the action: produce an ActionCandidate from the routing
|
||||
// decision. The candidate carries the resolved function, its arguments,
|
||||
// and where the resolution came from (route or matcher).
|
||||
candidate := h.resolveAction(ctx, dec)
|
||||
|
||||
// Write the candidate's resolved values back into Slots so the existing
|
||||
// branches (task-status, Praxis, Hexis, proposeGap, tool.Executor) work
|
||||
// unchanged. This is the mechanical adjustment that preserves all existing
|
||||
// behavior without redesigning those branches.
|
||||
if candidate.ActionResolved() {
|
||||
dec.Slots.Fn = candidate.Fn
|
||||
dec.Slots.Args = candidate.Args
|
||||
dec.Slots.HasFn = true
|
||||
}
|
||||
|
||||
// The board is Maven's own store, so a spoken status change is answered here
|
||||
|
||||
Reference in New Issue
Block a user