06adc4702d
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.
222 lines
8.8 KiB
Go
222 lines
8.8 KiB
Go
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 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,
|
|
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}
|
|
}
|