package router import "strings" // 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 // 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 } // 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 != "" } // --- structural validation --- // ActionField identifies a structural field of ActionCandidate for validation // reporting. Kept as a string enum so callers can switch on known values // without importing a large set. type ActionField string const ( FieldFn ActionField = "fn" FieldArgs ActionField = "args" ) // ActionValidationStatus is the typed classification of a validation outcome. // Five disjoint values: exactly one is set on every ActionValidationResult. type ActionValidationStatus string const ( // ActionValid — Fn is non-empty and the candidate is structurally // admissible. The caller may proceed to risk policy and execution. ActionValid ActionValidationStatus = "valid" // ActionUnresolved — Fn is empty. The matcher did not match, or the // decision was not an act. The caller routes to proposeGap / // clarification. This is NOT a structural error. ActionUnresolved ActionValidationStatus = "unresolved" // ActionMissingArgument — Fn is present but required arguments are // absent. Currently unused: no tool declares required args at the // candidate level. Preserved for future use without semantic change. ActionMissingArgument ActionValidationStatus = "missing_argument" // ActionInvalidArgument — Fn is present and arguments are present but // structurally malformed (e.g. wrong shape for a known tool). Currently // unused: argument shape is validated by the tool executor, not the // candidate validator. Preserved for future use. ActionInvalidArgument ActionValidationStatus = "invalid_argument" // ActionAmbiguousTarget — Fn is present but the entity/target reference // is ambiguous (multiple candidates, or a demonstrative with no // referent). Currently handled downstream by ecosystem handlers, not // the candidate validator. Preserved for future centralization. ActionAmbiguousTarget ActionValidationStatus = "ambiguous_target" ) // ActionValidationIssue records one structural problem found by // ValidateActionCandidate. Field names which field; Reason is a short // machine-readable tag, not a human sentence. type ActionValidationIssue struct { Field ActionField Reason string } // ActionValidationResult is the typed output of ValidateActionCandidate. // Exactly one of the five Status values is set. Issues carries detail for // invalid statuses; it is nil for valid and unresolved. // // The result answers only shape/completeness — not trust, confidence, risk, // or semantic correctness. Risk policy, confirmation, voice authority, and // irreversible-action policy remain downstream and unchanged. type ActionValidationResult struct { Status ActionValidationStatus Issues []ActionValidationIssue Missing []string `json:"Missing,omitempty"` } // Unresolved reports whether the candidate was not resolved (Fn empty). // Kept as a method for backward compatibility with existing call sites. func (r ActionValidationResult) Unresolved() bool { return r.Status == ActionUnresolved } // Valid reports whether the candidate is structurally admissible. // Kept as a method for backward compatibility with existing call sites. func (r ActionValidationResult) Valid() bool { return r.Status == ActionValid } // 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 resolved CapabilitySelection: the capability-selection stage // already resolved the function. Candidate source is ActionSourceRoute. // - Act without resolved CapabilitySelection: the fallback matcher runs // against the text slot. Candidate source is ActionSourceMatcher on match, // or Fn stays empty. // // The CapabilitySelection is the authoritative source for Fn/Args. The // compatibility fields on Decision.Slots (Fn/Args/HasFn) are still populated // for backward compatibility but are not read here. func ResolveActionCandidate(dec Decision, m ActMatcher) ActionCandidate { if dec.Intent != IntentAct { return ActionCandidate{} } // Capability selection resolved the function upstream. This is the // authoritative path for decisions produced by the router (which calls // SelectCapability). if dec.CapabilitySelection.Resolved { return ActionCandidate{ Fn: dec.CapabilitySelection.Fn, Args: dec.CapabilitySelection.Args, Source: ActionSourceRoute, Producer: dec.Producer, ResolvedBy: dec.CapabilitySelection.Method, Confidence: dec.Confidence, } } // Backward compatibility: decisions constructed outside the router // (tests, rebuilt decisions) may set Slots.HasFn without // CapabilitySelection. Read the compatibility fields. if dec.Slots.HasFn { return ActionCandidate{ Fn: dec.Slots.Fn, Args: dec.Slots.Args, Source: ActionSourceRoute, Producer: dec.Producer, ResolvedBy: dec.Slots.ResolvedBy, 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, ResolvedBy: ActionResolutionFallbackMatcher, Confidence: dec.Confidence, } } } return ActionCandidate{ Producer: dec.Producer, Confidence: dec.Confidence, } } // ValidateActionCandidate checks whether a resolved ActionCandidate is // structurally admissible for the current execution path. It answers only // shape/completeness — not trust, confidence, risk, or semantic correctness. // // Five outcomes (exactly one): // - Unresolved (Fn empty): the matcher missed. Caller routes to // proposeGap / clarification. This is NOT a validation error. // - Valid (Fn non-empty, structure sound): candidate may proceed to // risk policy and execution. // - InvalidArgument (Fn present but args malformed): candidate must not // execute. Currently unused; preserved for future use. // - MissingArgument (Fn present but required args absent): candidate must // not execute. Currently unused; preserved for future use. // - AmbiguousTarget (Fn present but target ambiguous): candidate must not // execute. Currently handled downstream; preserved for future use. // // Validation does not re-run routing, intent classification, matcher // resolution, language parsing, or entity inference. func ValidateActionCandidate(c ActionCandidate) ActionValidationResult { // Unresolved: Fn is empty. The matcher did not match, or the decision // was not an act. This flows into the existing propose-gap / proposal // path and must NOT be treated as a structural error. if c.Fn == "" { return ActionValidationResult{Status: ActionUnresolved} } var issues []ActionValidationIssue // Fn is present but must be a non-blank identifier. The route and // matcher both produce bare function names; whitespace-only or // control-character Fn would be a structural defect in the producer. if strings.TrimSpace(c.Fn) == "" { issues = append(issues, ActionValidationIssue{ Field: FieldFn, Reason: "blank_function_name", }) } if len(issues) > 0 { return ActionValidationResult{Status: ActionInvalidArgument, Issues: issues} } return ActionValidationResult{Status: ActionValid} }