router: set ResolvedBy at each function selection point

Assign provenance where the exact fn is produced:
- grammar_fixed: praxis/task-status grammars hardcode fn
- grammar_matcher: wakeword-act grammar invokes ActMatcher
- extractor_raw: Extractor.Extract matches over raw utterance
- extractor_llm_text: fillSlots LLM backfill matches cleaned text
- fallback_matcher: ResolveActionCandidate runs the fallback matcher

ResolveActionCandidate propagates Slots.ResolvedBy into
ActionCandidate.ResolvedBy. No selection behavior changes.
This commit is contained in:
2026-09-06 13:50:03 +04:00
parent 1d02ba8936
commit 06adc4702d
6 changed files with 27 additions and 18 deletions
+7
View File
@@ -22,6 +22,11 @@ type ActionCandidate struct {
// here. Carried for observability; not used for dispatch.
Producer RouteProducer
// ResolvedBy — which component actually selected the exact function.
// Carried from the routing decision's Slots.ResolvedBy. Five disjoint
// values; empty when no function was resolved.
ResolvedBy ActionResolutionMethod
// Confidence — the routing confidence from the decision. Carried for
// observability; not used for dispatch.
Confidence float64
@@ -146,6 +151,7 @@ func ResolveActionCandidate(dec Decision, m ActMatcher) ActionCandidate {
Args: dec.Slots.Args,
Source: ActionSourceRoute,
Producer: dec.Producer,
ResolvedBy: dec.Slots.ResolvedBy,
Confidence: dec.Confidence,
}
}
@@ -158,6 +164,7 @@ func ResolveActionCandidate(dec Decision, m ActMatcher) ActionCandidate {
Args: args,
Source: ActionSourceMatcher,
Producer: dec.Producer,
ResolvedBy: ActionResolutionFallbackMatcher,
Confidence: dec.Confidence,
}
}
+5 -5
View File
@@ -214,7 +214,7 @@ func PraxisGrammars() []Grammar {
Stage: 0,
Intent: IntentAct,
Confidence: 1.0,
Slots: Slots{Fn: c.Fn, HasFn: true, Value: c.Ref},
Slots: Slots{Fn: c.Fn, HasFn: true, Value: c.Ref, ResolvedBy: ActionResolutionGrammarFixed},
}, true
},
},
@@ -226,7 +226,7 @@ func PraxisGrammars() []Grammar {
Stage: 0,
Intent: IntentAct,
Confidence: 1.0,
Slots: Slots{Fn: "list_attention", HasFn: true},
Slots: Slots{Fn: "list_attention", HasFn: true, ResolvedBy: ActionResolutionGrammarFixed},
}, true
},
},
@@ -238,7 +238,7 @@ func PraxisGrammars() []Grammar {
Stage: 0,
Intent: IntentAct,
Confidence: 1.0,
Slots: Slots{Fn: "list_changes", HasFn: true},
Slots: Slots{Fn: "list_changes", HasFn: true, ResolvedBy: ActionResolutionGrammarFixed},
}, true
},
},
@@ -261,7 +261,7 @@ func PraxisGrammars() []Grammar {
// Text, not Value: entityAttentionCapability reads Value
// first and that slot means an item id everywhere else in
// the Praxis dispatch.
Slots: Slots{Fn: "entity_attention", HasFn: true, Text: subject},
Slots: Slots{Fn: "entity_attention", HasFn: true, Text: subject, ResolvedBy: ActionResolutionGrammarFixed},
}, true
},
},
@@ -284,7 +284,7 @@ func praxisServiceAttentionDecision(utterance string) (Decision, bool) {
Stage: 0,
Intent: IntentAct,
Confidence: 1,
Slots: Slots{Fn: "entity_attention", HasFn: true, Text: tokens[3]},
Slots: Slots{Fn: "entity_attention", HasFn: true, Text: tokens[3], ResolvedBy: ActionResolutionGrammarFixed},
}, true
}
+2 -1
View File
@@ -303,7 +303,7 @@ func (r *Router) fillMatchedSlots(ctx context.Context, d *Decision, now time.Tim
d.Slots.Key, d.Slots.Value, d.Slots.HasKey = ex.Key, ex.Value, ex.HasKey
}
if !d.Slots.HasFn && ex.HasFn {
d.Slots.Fn, d.Slots.Args, d.Slots.HasFn = ex.Fn, ex.Args, ex.HasFn
d.Slots.Fn, d.Slots.Args, d.Slots.HasFn, d.Slots.ResolvedBy = ex.Fn, ex.Args, ex.HasFn, ex.ResolvedBy
}
return ex
}
@@ -320,6 +320,7 @@ func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) {
d.Slots.Text != "" && d.Slots.Text != d.Utterance {
if fn, args, ok := r.extractor.Acts.Match(d.Slots.Text); ok {
d.Slots.Fn, d.Slots.Args, d.Slots.HasFn = fn, args, true
d.Slots.ResolvedBy = ActionResolutionExtractorLLMText
}
}
// The extractor's Text is the raw utterance, which is the payload for a
+1
View File
@@ -73,6 +73,7 @@ func (e Extractor) Extract(ctx context.Context, intent Intent, utterance string,
s.Fn = fn
s.Args = args
s.HasFn = true
s.ResolvedBy = ActionResolutionExtractorRaw
}
}
case IntentFact:
+1 -1
View File
@@ -99,7 +99,7 @@ func DefaultGrammars(actMatcher ActMatcher) []Grammar {
Stage: 0,
Intent: IntentAct,
Confidence: 1.0,
Slots: Slots{Fn: fn, Args: args, HasFn: true, Text: rest},
Slots: Slots{Fn: fn, Args: args, HasFn: true, Text: rest, ResolvedBy: ActionResolutionGrammarMatcher},
}, true
},
},
+1 -1
View File
@@ -389,7 +389,7 @@ func TaskStatusGrammar() Grammar {
// which is the pairing handlePraxisAct uses for an item and its
// reference. Empty Text is a claim, not a refusal: the daemon
// asks which task, having the list she does not.
Slots: Slots{Fn: TaskStatusFn, HasFn: true, Value: c.Status, Text: c.Text},
Slots: Slots{Fn: TaskStatusFn, HasFn: true, Value: c.Status, Text: c.Text, ResolvedBy: ActionResolutionGrammarFixed},
}, true
},
}