package router // ActionCandidate — the result of action resolution, produced before execution. // It replaces the implicit ownership split where the router filled Slots.Fn/Args // and actionAct re-matched when they were absent. One candidate is produced per // IntentAct decision, carrying the resolved function, its arguments, and where // the resolution came from. type ActionCandidate struct { // Fn — the resolved function/tool identity. Empty when no match was found. 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 // Source — where the resolution came from. Typed enum, not free-form. Source ActionSource // 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 } // ActionSource — where action resolution came from. Two values: the router // resolved the function upstream (stage-0 grammar or stage-2 extraction), or // the fallback matcher ran because the router did not fill Fn. type ActionSource string const ( // ActionSourceRoute — Fn/Args were already resolved in the routing // cascade (stage-0 grammar match, stage-2 extractor, or LLM slot // backfill). The matcher was not invoked. ActionSourceRoute ActionSource = "route" // ActionSourceMatcher — the router left Fn empty, so the fallback // matcher ran against the text slot and produced the match. ActionSourceMatcher ActionSource = "matcher" ) // ActionResolved reports whether the candidate resolved to a function. func (c ActionCandidate) ActionResolved() bool { return c.Fn != "" } // ResolveActionCandidate produces an ActionCandidate from a routing decision. // It is the single boundary between routing and action resolution: everything // downstream consumes the candidate rather than re-resolving the function. // // Resolution rules: // - Non-act intents: candidate is not applicable (Fn empty, source empty). // - Act with Slots.HasFn: the router already resolved the function upstream // (stage-0 grammar, stage-2 extractor, or LLM slot backfill). Candidate // source is ActionSourceRoute. // - Act without Fn: the fallback matcher runs against the text slot. // Candidate source is ActionSourceMatcher on match, or Fn stays empty. // // The matcher algorithm, enabled-tool set, alias behavior, fuzzy-prefix // behavior, and ordering are unchanged — this is a mechanical extraction of // the same matching call that actionAct previously owned. func ResolveActionCandidate(dec Decision, m ActMatcher) ActionCandidate { if dec.Intent != IntentAct { return ActionCandidate{} } // Router resolved the function upstream. if dec.Slots.HasFn { return ActionCandidate{ Fn: dec.Slots.Fn, Args: dec.Slots.Args, Source: ActionSourceRoute, Producer: dec.Producer, Confidence: dec.Confidence, } } // Fallback: invoke the matcher against the text slot. if dec.Slots.Text != "" && m != nil { if fn, args, ok := m.Match(dec.Slots.Text); ok { return ActionCandidate{ Fn: fn, Args: args, Source: ActionSourceMatcher, Producer: dec.Producer, Confidence: dec.Confidence, } } } return ActionCandidate{ Producer: dec.Producer, Confidence: dec.Confidence, } }