package router // CapabilitySelection — the result of the explicit capability-selection stage. // It says what executable capability matched, separate from what kind of turn // this is (RouteDecision/Decision) and separate from the downstream action // artifact (ActionCandidate). // // CapabilitySelection is the authoritative record of which exact function was // selected and by which component. Decision.Slots.Fn/Args/HasFn remain as // compatibility representations that are populated FROM the selection; later // action execution must not depend on those compatibility fields. type CapabilitySelection struct { // Fn — the resolved function/tool identity. Empty when no capability // matched the input. Fn string // Args — positional arguments passed to the tool. May be nil when Fn // is empty or when the match produced no arguments. Args []string // Resolved — whether a capability was matched. Fn may be non-empty // even when Resolved is false (grammar-fixed paths set Fn without // going through the general selector). This field distinguishes "the // selector ran and matched" from "a deterministic path set Fn". Resolved bool // Method — which component actually selected the function. Five // disjoint values from ActionResolutionMethod; empty when no function // was resolved. Method ActionResolutionMethod // InputKind — what the selector selected against. Distinguishes the // raw utterance from LLM-cleaned text, so the observability trace // can name the exact input the matcher saw. InputKind SelectionInputKind // Producer — which cascade stage produced the routing decision that // led here. Carried for observability; not used for dispatch. Producer RouteProducer // Confidence — the routing confidence from the decision. Carried for // observability; not used for dispatch. Confidence float64 } // SelectionInputKind — what the selector selected against. Three disjoint // values. type SelectionInputKind string const ( // SelectionRaw — the selector matched against the original raw // utterance from the user. SelectionRaw SelectionInputKind = "raw" // SelectionLLMText — the selector matched against LLM-normalized or // cleaned action text from Slots.Text, which may differ from the raw // utterance. SelectionLLMText SelectionInputKind = "llm_text" // SelectionDeterministic — the selector was bypassed because a // deterministic grammar already resolved the function. The selector // did not run; this records that the bypass happened. SelectionDeterministic SelectionInputKind = "deterministic" ) // applyCapabilityToSlots propagates the CapabilitySelection result into the // Decision.Slots compatibility fields. This preserves backward compatibility // for code that still reads Slots.Fn/Args/HasFn, while CapabilitySelection // remains the authoritative record. Later action execution must read the // candidate produced from CapabilitySelection, not these compatibility fields. func applyCapabilityToSlots(dec *Decision, sel CapabilitySelection) { dec.CapabilitySelection = sel if sel.Resolved { dec.Slots.Fn = sel.Fn dec.Slots.Args = sel.Args dec.Slots.HasFn = true dec.Slots.ResolvedBy = sel.Method } } // SelectCapability is the explicit capability-selection stage. It sits between // route resolution and action candidate production, answering: which exact // executable capability matched this turn? // // Resolution order: // 1. HasFn already set (grammar-fixed or extractor raw): bypass the general // selector. CapabilitySelection records the existing result with // InputKind=SelectionDeterministic. // 2. Raw utterance not matching the allowlist, but LLM cleaned text available: // try Acts.Match(LLM text). This is the extractor_llm_text path. // 3. No match on either input: unresolved. // // The matcher algorithm, enabled-tool set, alias behavior, fuzzy-prefix // behavior, and ordering are all unchanged — SelectCapability delegates to // the same Acts.Match call that Extract and ResolveActionCandidate always used. func SelectCapability(dec Decision, m ActMatcher) CapabilitySelection { // Non-act intents have no capability to select. if dec.Intent != IntentAct { return CapabilitySelection{ Producer: dec.Producer, Confidence: dec.Confidence, } } // Deterministic path: a grammar or the raw extractor already resolved // the function. The general selector does not re-run. if dec.Slots.HasFn { return CapabilitySelection{ Fn: dec.Slots.Fn, Args: dec.Slots.Args, Resolved: true, Method: dec.Slots.ResolvedBy, InputKind: SelectionDeterministic, Producer: dec.Producer, Confidence: dec.Confidence, } } // General path: the raw utterance did not match. Try the LLM-cleaned // text when it differs from the raw utterance. if m != nil && dec.Slots.Text != "" && dec.Slots.Text != dec.Utterance { if fn, args, ok := m.Match(dec.Slots.Text); ok { return CapabilitySelection{ Fn: fn, Args: args, Resolved: true, Method: ActionResolutionExtractorLLMText, InputKind: SelectionLLMText, Producer: dec.Producer, Confidence: dec.Confidence, } } } // Unresolved: no capability matched on any input. return CapabilitySelection{ Producer: dec.Producer, Confidence: dec.Confidence, } }