6.5 KiB
Action-Resolution Boundary — Slice Report
1. Files changed
| File | Status | Purpose |
|---|---|---|
internal/router/actioncandidate.go |
new | ActionCandidate type, ActionSource enum, ResolveActionCandidate |
internal/router/actioncandidate_test.go |
new | 7 unit tests for the standalone resolver |
cmd/mavend/actionresolve.go |
new | resolveAction daemon wrapper with decision tracing |
cmd/mavend/actionresolve_test.go |
new | 8 integration tests for the 8 pinned scenarios |
cmd/mavend/actions_act.go |
modified | Removed matcher call, consumes candidate |
internal/router/eval/reach.go |
modified | Reach() uses ResolveActionCandidate |
2. ActionCandidate contract
type ActionCandidate struct {
Fn string // resolved function/tool identity (empty = unresolved)
Args []string // positional arguments (may be nil)
Source ActionSource // "route" or "matcher"
Producer RouteProducer // cascade stage that produced the decision
Confidence float64 // routing confidence
}
type ActionSource string
const (
ActionSourceRoute ActionSource = "route" // Fn/Args resolved upstream
ActionSourceMatcher ActionSource = "matcher" // fallback matcher resolved
)
3. Where resolveAction lives and why
- Standalone:
router.ResolveActionCandidate(dec, m)ininternal/router/actioncandidate.go— reusable by both daemon and eval harness. - Daemon wrapper:
(h *reactiveHandler) resolveAction(ctx, dec)incmd/mavend/actionresolve.go— delegates to the standalone function, adds decision tracing.
Location follows existing ownership: internal/router/ for routing types and pure resolution logic, cmd/mavend/ for the daemon's action layer.
4. Before/after act flow
Before:
RouteDecision → actionAct
├─ if !HasFn: h.matcher.Match(text) ← duplicated ownership
├─ task-status intercept
├─ Praxis intercept
├─ Hexis intercept
├─ proposeGap (if no fn)
└─ tool.Executor.Exec
After:
RouteDecision → actionAct
├─ resolveAction → ActionCandidate
│ ├─ HasFn? → source=route
│ └─ else? → h.matcher.Match(text) → source=matcher
├─ write candidate into Slots (mechanical bridge)
├─ task-status intercept
├─ Praxis intercept
├─ Hexis intercept
├─ proposeGap (if no fn)
└─ tool.Executor.Exec
5. Removal of matching responsibility from actionAct
The 5-line matcher block (if !dec.Slots.HasFn && dec.Slots.Text != "" && h.matcher != nil { ... }) was removed from actionAct. It now calls resolveAction which delegates to router.ResolveActionCandidate. The candidate's resolved values are written back into dec.Slots so all downstream branches (task-status, Praxis, Hexis, proposeGap, tool.Executor) work unchanged.
6. Tests added
Router unit tests (internal/router/actioncandidate_test.go):
TestResolveActionCandidate_RouteSource— HasFn=true → source=routeTestResolveActionCandidate_MatcherSource— no Fn → matcher resolvesTestResolveActionCandidate_MatcherMiss— no Fn → matcher miss → unresolvedTestResolveActionCandidate_NonAct— non-act → empty candidateTestResolveActionCandidate_Stage0Match— stage-0 → route, confidence=1.0TestResolveActionCandidate_LearnedRouterNoFn— LLM no Fn → matcher fallbackTestResolveActionCandidate_AliasMatch— alias resolves through matcher
Daemon integration tests (cmd/mavend/actionresolve_test.go):
TestActRouteSource_NoMatcherInvoke— act with HasFn=true, route-sourcedTestActMatcherSource_FallbackMatch— act without Fn, matcher-sourcedTestActMatcherMiss_ProposeGap— matcher miss → propose-gapTestActDestructive_ConfirmationUnchanged— destructive → confirmTestActTaskStatus_InterceptUnchanged— task-status interceptedTestActStage0_SameResult— stage-0 act executes same toolTestActLearnedRouter_NoFn_FallbackMatch— learned-router no Fn → matcherTestResolveAction_CandidateSource_Verified— verifies source for all paths
7. Before/after test results
ok github.com/kami/maven/cmd/mavend 22.149s
ok github.com/kami/maven/internal/router 1.131s
ok github.com/kami/maven/internal/router/eval 0.762s
All 15 new tests pass. All existing tests pass unchanged.
8. Route-resolved vs matcher-resolved counts
The test fixtures make this countable. From the 8 new integration tests:
- Route-resolved: 3 (TestActRouteSource, TestActStage0, TestResolveAction_CandidateSource route branch)
- Matcher-resolved: 3 (TestActMatcherSource, TestActLearnedRouter, TestResolveAction_CandidateSource matcher branch)
- Matcher miss: 2 (TestActMatcherMiss, TestResolveAction_CandidateSource miss branch)
The decision trace records action-resolve:route:<fn> or action-resolve:matcher:<fn> for every act turn, making production measurement possible via the existing /trace endpoint.
9. Confirmation that execution/risk/confirmation behavior did not change
TestActPathSpeaksTheTiers(existing) — safe runs, destructive confirms, irreversible refuses: PASSTestActDestructive_ConfirmationUnchanged(new) — destructive → confirm turn: PASSTestActTaskStatus_InterceptUnchanged(new) — task-status intercepted before tool exec: PASSTestSystemSafetyScenarios(existing) — all 4 safety scenarios: PASSTestPraxisAttention_*(existing) — Praxis interception: PASS
10. Ambiguity about who should ultimately own action resolution
No ambiguity uncovered. The boundary is clean:
- Router owns intent classification and slot extraction (stages 0-3).
ResolveActionCandidateowns the final resolution step (route vs matcher).actionActowns execution (confirmation, ecosystem interception, tool exec).
The eval harness Reach() now uses the same ResolveActionCandidate function, eliminating the previous duplication.
11. Commit hashes
Three commits, split to stay under the 300-line pre-commit cap:
f6d7b05 mavend: add action-resolution regression tests
025f81e mavend: wire resolveAction into actionAct
064747f router: add ActionCandidate type and ResolveActionCandidate