Files
Agent-Intent/intent/slots.go
T
weichenandCursor 3130190acb
ci / vet-and-test (pull_request) Failing after 2s
ci / docker-checkout-hostgateway (pull_request) Successful in 2s
Reject invalid classifier refinements and scope slot validators
Why: Refine could swap handlers or emit out-of-range scores, Ambiguous tops skipped slot extraction, and the process-global validator map coupled independent registries.
Evidence: GOWORK=off go vet ./... && go test -count=1 ./... && go test -race ./... passed, including TestRecognizeClassifierRejectsBadRefinement, TestRecognizeClassifierRebuildsTamperedHandler, TestRecognizeClassifierRefinedTopGetsSlots, and TestScopedSlotValidatorsDoNotInterfere. error-code-system and logging-system advanced to a3ee67b and 49b5d8b (docs-only).
Impact: illegal Refine results stay Ambiguous; a resolved Top is slotted; Options.SlotValidators wins over the deprecated global table.
Refs: I1, I2, I6
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 00:47:16 +00:00

162 lines
4.7 KiB
Go

package intent
import (
"regexp"
"strings"
"sync"
)
// SlotValidator 宿主注册的槽位值校验器。必须纯函数、确定性、无 I/O。
type SlotValidator func(value string) error
var slotValidators sync.Map // name → SlotValidator
// RegisterSlotValidator 注册进程级具名校验器;spec 通过 SlotSpec.Validator 引用。
// 必须在快照构建前注册(构建期会校验名字存在)。
//
// Deprecated: 校验器应随 Registry 隔离。请改用 Options.SlotValidators。
// 全局表只作回落:构建与校验先查作用域表,查不到再查这里。
func RegisterSlotValidator(name string, fn SlotValidator) {
slotValidators.Store(name, fn)
}
// LookupSlotValidator 按名查找已注册的全局校验器。
//
// Deprecated: 请改用 Options.SlotValidators。本函数只服务全局回落。
func LookupSlotValidator(name string) (SlotValidator, bool) {
fn, ok := slotValidators.Load(name)
if !ok {
return nil, false
}
v, ok := fn.(SlotValidator)
return v, ok
}
// resolveSlotValidator 先查作用域表,查不到再回落已弃用的全局表。
func resolveSlotValidator(scoped map[string]SlotValidator, name string) (SlotValidator, bool) {
if fn, ok := scoped[name]; ok && fn != nil {
return fn, true
}
return LookupSlotValidator(name)
}
// 内置归一化步骤:trim / lower / upper。
var builtinNormalizers = map[string]func(string) string{
"trim": strings.TrimSpace,
"lower": strings.ToLower,
"upper": strings.ToUpper,
}
func normalizeSlotValue(v string, ops []string) string {
for _, op := range ops {
if fn, ok := builtinNormalizers[op]; ok {
v = fn(v)
}
}
return v
}
// compiledSlot 编译后的槽位抽取与校验产物。
type compiledSlot struct {
key string
required bool
label string
question string
re *regexp.Regexp
group int
enumSet map[string]bool
enum []string
formatRe *regexp.Regexp
normalize []string
validator SlotValidator
}
// validate 对归一化后的值做确定性校验;返回(原因, 是否合法)。
func (s *compiledSlot) validate(v string) (string, bool) {
if s.formatRe != nil && !s.formatRe.MatchString(v) {
return "format mismatch: " + s.formatRe.String(), false
}
if s.enumSet != nil && !s.enumSet[v] {
return "value not in enum", false
}
if s.validator != nil {
if err := s.validator(v); err != nil {
return "validator: " + err.Error(), false
}
}
return "", true
}
// extractSlots 抽取并归一化槽位。
// missing:必填槽位未捕获(模式没匹配上);invalid:捕获到但校验不合法(key→原因)。
// 必填且非法只进 invalid(宿主可区分"没给"与"给错了"来决定澄清话术)。
func (cs *compiledSpec) extractSlots(input string) (slots map[string]string, missing []string, invalid map[string]string) {
if len(cs.slots) == 0 {
return nil, nil, nil
}
for _, s := range cs.slots {
m := s.re.FindStringSubmatch(input)
if m == nil || s.group >= len(m) || m[s.group] == "" {
if s.required {
missing = append(missing, s.key)
}
continue
}
v := normalizeSlotValue(m[s.group], s.normalize)
if reason, ok := s.validate(v); !ok {
if invalid == nil {
invalid = map[string]string{}
}
invalid[s.key] = reason
continue
}
if slots == nil {
slots = map[string]string{}
}
slots[s.key] = v
}
return slots, missing, invalid
}
func compileSlot(spec SlotSpec, scoped map[string]SlotValidator) (compiledSlot, error) {
re, err := regexp.Compile(spec.Pattern)
if err != nil {
return compiledSlot{}, newError(InvalidSpec, "slot pattern 编译失败",
"field", "slots", "key", spec.Key, "error", err.Error())
}
cs := compiledSlot{
key: spec.Key,
required: spec.Required,
label: spec.Label,
question: spec.Question,
re: re,
group: effectiveGroup(spec.Group),
normalize: append([]string(nil), spec.Normalize...),
}
if len(spec.Enum) > 0 {
cs.enum = append([]string(nil), spec.Enum...)
cs.enumSet = make(map[string]bool, len(spec.Enum))
for _, e := range spec.Enum {
cs.enumSet[e] = true
}
}
if spec.Format != "" {
fre, err := regexp.Compile(spec.Format)
if err != nil {
return compiledSlot{}, newError(InvalidSpec, "slot format 编译失败",
"field", "slots", "key", spec.Key, "format", spec.Format, "error", err.Error())
}
cs.formatRe = fre
}
if spec.Validator != "" {
fn, ok := resolveSlotValidator(scoped, spec.Validator)
if !ok {
return compiledSlot{}, newError(InvalidSpec, "slot validator 未注册",
"field", "slots", "key", spec.Key, "validator", spec.Validator,
"hint", "在 Options.SlotValidators 登记,或调用已弃用的 intent.RegisterSlotValidator")
}
cs.validator = fn
}
return cs, nil
}