From 8d83c66d1119329557bbda15dd012c0f45c410b2 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 6 Sep 2026 21:02:05 +0400 Subject: [PATCH] router: wire SelectCapability into Router.Route pipeline (slice 6b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Call SelectCapability after each cascade path (grammar, heads, LLM, classifier) and propagate the result via applyCapabilityToSlots. Remove LLM text capability backfill from fillSlots — SelectCapability now owns that path. fillMatchedSlots retains raw extractor capability extraction for backward compatibility with stage-0 grammars. gateLLMDecision now reads CapabilitySelection.Resolved instead of Slots.HasFn for the act-intent confidence thinning check. --- internal/router/router.go | 45 +++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/internal/router/router.go b/internal/router/router.go index bd83e82..93265a3 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -105,6 +105,11 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De // The grammar decided the intent; the extractor fills the slots it did // not match (V-572). See fillMatchedSlots for why every grammar gets it. r.fillMatchedSlots(ctx, &d, now) + // Capability selection: deterministic grammars that already resolved Fn + // (wakeword-act, praxis, task-status) bypass the general selector. + // SelectCapability records the existing result with + // InputKind=SelectionDeterministic. + applyCapabilityToSlots(&d, SelectCapability(d, r.extractor.Acts)) r.noteGrammarOutcomes(ctx, i+1, declinedBuild, g.Name, d.Intent) return d, nil } @@ -142,6 +147,7 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De Producer: RouteProducerHeads, } r.fillSlots(ctx, &d, now) + applyCapabilityToSlots(&d, SelectCapability(d, r.extractor.Acts)) // The clarify head relearned the English assumption that one word // cannot be a sentence. Russian verbs carry subject and tense, and a // deterministic fact parser which also found a key gives both halves @@ -184,6 +190,13 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De d.Utterance = utterance d.Producer = RouteProducerLLM r.fillSlots(ctx, &d, now) + // Capability selection: after fillSlots populates Text, SelectCapability + // tries the LLM-cleaned text against the allowlist when the raw + // utterance did not match. This replaces the former LLM text backfill + // that was embedded in fillSlots. Must run before gateLLMDecision so + // the gate sees the correct resolution state for acts. + sel := SelectCapability(d, r.extractor.Acts) + applyCapabilityToSlots(&d, sel) before := d.Confidence r.gateLLMDecision(&d) // The classifier is the floor and it never ran, which is the whole @@ -244,6 +257,12 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De Slots: r.extractor.Extract(ctx, best.Intent, utterance, now), Producer: RouteProducerClassifier, } + // Capability selection: for the classifier path, the raw extractor + // already filled Slots.Fn/Args via Extract(IntentAct). SelectCapability + // records this as a deterministic bypass. If the raw match missed, + // there is no LLM text to fall back to (Text == Utterance), so the + // selection remains unresolved. + applyCapabilityToSlots(&d, SelectCapability(d, r.extractor.Acts)) // stage 3 — confidence gate. Below threshold ⇒ clarify, don't guess. if d.Confidence < r.threshold { @@ -302,27 +321,22 @@ func (r *Router) fillMatchedSlots(ctx context.Context, d *Decision, now time.Tim if !d.Slots.HasKey && ex.HasKey { d.Slots.Key, d.Slots.Value, d.Slots.HasKey = ex.Key, ex.Value, ex.HasKey } + // Capability selection (Fn/Args/HasFn) is now handled by + // SelectCapability, not here. The extractor still fills them via + // Extract(IntentAct), but the authoritative record lives in + // Decision.CapabilitySelection. See capability.go. if !d.Slots.HasFn && ex.HasFn { d.Slots.Fn, d.Slots.Args, d.Slots.HasFn, d.Slots.ResolvedBy = ex.Fn, ex.Args, ex.HasFn, ex.ResolvedBy } return ex } -// fillSlots — fillMatchedSlots for an LLM decision, plus the two backfills that -// only make sense there. The LLM wins where it answered: it saw the sentence, -// the parsers are keyword tables. +// fillSlots — fillMatchedSlots for an LLM decision, plus the text backfill. +// Capability selection (Fn/Args) is now handled by SelectCapability, called +// after fillSlots in the routing pipeline. This function fills only time, key, +// and text slots. func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) { ex := r.fillMatchedSlots(ctx, d, now) - // For an act the model returns the verb in Text ("restart nginx"), which is - // often cleaner than the raw utterance ("maven, could you restart nginx"). - // Try it too when the utterance did not match the allowlist. - if d.Intent == IntentAct && !d.Slots.HasFn && r.extractor.Acts != nil && - 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 // note, a query or a chat turn but not for a reminder — there Text is the // subject, what she says at the hour. Backfilling it made Text impossible @@ -354,11 +368,14 @@ func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) { // Anything below threshold gets the exact same Clarify=true treatment the // classifier path already produces — same field, same daemon-side consumer // (cmd/mavend/clarify.go), nothing new to wire. +// +// The act check now reads CapabilitySelection.Resolved (set by +// SelectCapability before this function runs) rather than Slots.HasFn. func (r *Router) gateLLMDecision(d *Decision) { if d.Intent == IntentFact && !d.Slots.HasKey && d.Confidence > llmThinConfidence { d.Confidence = llmThinConfidence } - if d.Intent == IntentAct && !d.Slots.HasFn && d.Confidence > llmThinConfidence { + if d.Intent == IntentAct && !d.CapabilitySelection.Resolved && d.Confidence > llmThinConfidence { d.Confidence = llmThinConfidence } // A reminder with no subject: she knows when but not what to say then.