Generate 2–4 step L1 proposals with stable digests while leaving TopIntent and the legacy IntentPlan router unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
304 lines
8.1 KiB
Go
304 lines
8.1 KiB
Go
package intent
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
func attachPlanProposal(ctx context.Context, r *RuleBasedRecognizer, input string, snap *ContextSnapshot, res *IntentRecognitionResult) {
|
|
if res == nil {
|
|
return
|
|
}
|
|
proposal := buildPlanProposal(ctx, r, input, snap, res)
|
|
if proposal == nil {
|
|
return
|
|
}
|
|
res.PlanProposal = proposal
|
|
res.PlanShadow = buildPlanShadow(res, proposal)
|
|
}
|
|
|
|
func buildPlanProposal(ctx context.Context, r *RuleBasedRecognizer, input string, snap *ContextSnapshot, primary *IntentRecognitionResult) *PlanProposal {
|
|
trimmed := strings.TrimSpace(input)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
if primary != nil && primary.TopIntent != nil && primary.TopIntent.Intent == IntentSecurityForbidden {
|
|
return rejectedProposal(trimmed, ComboRejected, "security_redline")
|
|
}
|
|
if isSecurityViolation(trimmed) && !isNegatedWriteWithReadonlyIntent(trimmed) {
|
|
return rejectedProposal(trimmed, ComboRejected, "security_redline")
|
|
}
|
|
|
|
rawSegs := SplitPlanSegments(input)
|
|
if len(rawSegs) == 0 {
|
|
return nil
|
|
}
|
|
if len(rawSegs) > maxPlanSegments {
|
|
p := &PlanProposal{
|
|
SegmenterVersion: SegmenterVersion,
|
|
ComboHint: ComboRejectedBatchSize,
|
|
Evidence: []IntentEvidence{{Kind: "batch_size", Detail: fmt.Sprintf("%d", len(rawSegs)), Weight: 1}},
|
|
}
|
|
for i, s := range rawSegs {
|
|
p.Segments = append(p.Segments, proposedFromInput(s, i, nil, nil))
|
|
}
|
|
p.Digest = proposalDigest(p)
|
|
return p
|
|
}
|
|
|
|
proposed := make([]ProposedSegment, 0, len(rawSegs))
|
|
for i, s := range rawSegs {
|
|
text := s.Normalized
|
|
if text == "" {
|
|
text = strings.TrimSpace(s.Text)
|
|
}
|
|
var frame *IntentFrameV2
|
|
var missing []string
|
|
reusePrimary := len(rawSegs) == 1 && primary != nil && (text == "" || text == normalizeSegmentText(input) || text == strings.TrimSpace(input))
|
|
if reusePrimary {
|
|
frame = primary.IntentFrameV2
|
|
if frame == nil {
|
|
frame = BuildIntentFrameV2(primary, DefaultRegistry())
|
|
}
|
|
if primary.TopIntent != nil {
|
|
missing = append([]string{}, primary.TopIntent.MissingSlots...)
|
|
}
|
|
} else {
|
|
leaf, err := r.recognizeCore(ctx, text, snap)
|
|
if err == nil && leaf != nil {
|
|
frame = BuildIntentFrameV2(leaf, DefaultRegistry())
|
|
if leaf.TopIntent != nil {
|
|
missing = append([]string{}, leaf.TopIntent.MissingSlots...)
|
|
}
|
|
if frame != nil && len(frame.MissingSlots) > 0 {
|
|
missing = append(missing, frame.MissingSlots...)
|
|
}
|
|
if leaf.TopIntent != nil && leaf.TopIntent.Intent == IntentSecurityForbidden {
|
|
p := rejectedProposal(trimmed, ComboRejected, "segment_security")
|
|
p.Segments = append(p.Segments, proposedFromInput(s, i, frame, missing))
|
|
p.Digest = proposalDigest(p)
|
|
return p
|
|
}
|
|
}
|
|
}
|
|
if frame != nil && len(frame.MissingSlots) > 0 {
|
|
missing = append(missing, frame.MissingSlots...)
|
|
}
|
|
seg := proposedFromInput(s, i, frame, uniqueStrings(missing))
|
|
if i > 0 {
|
|
seg.UnresolvedAnaphora = collectAnaphora(text)
|
|
seg.DependsOn = []string{fmt.Sprintf("%d", i)}
|
|
}
|
|
proposed = append(proposed, seg)
|
|
}
|
|
|
|
p := &PlanProposal{
|
|
SegmenterVersion: SegmenterVersion,
|
|
Segments: proposed,
|
|
}
|
|
p.ComboHint = comboHintFor(p.Segments, primary)
|
|
p.Evidence = comboEvidence(p)
|
|
p.Digest = proposalDigest(p)
|
|
return p
|
|
}
|
|
|
|
func proposedFromInput(s InputSegment, i int, frame *IntentFrameV2, missing []string) ProposedSegment {
|
|
return ProposedSegment{
|
|
StepID: fmt.Sprintf("%d", i+1),
|
|
Text: s.Text,
|
|
Normalized: s.Normalized,
|
|
Span: s.Span,
|
|
Connector: s.Connector,
|
|
Evidence: s.Evidence,
|
|
StructureConfidence: s.StructureConfidence,
|
|
Frame: frame,
|
|
MissingSlots: missing,
|
|
}
|
|
}
|
|
|
|
func comboHintFor(segs []ProposedSegment, primary *IntentRecognitionResult) ComboHint {
|
|
if len(segs) <= 1 {
|
|
return ComboSingle
|
|
}
|
|
needsClarify := false
|
|
needsConfirm := false
|
|
allQuery := true
|
|
seenKeys := map[string]int{}
|
|
for _, s := range segs {
|
|
if s.StructureConfidence == "low" || len(s.UnresolvedAnaphora) > 0 || len(s.MissingSlots) > 0 || s.Frame == nil {
|
|
needsClarify = true
|
|
}
|
|
if s.Frame == nil {
|
|
allQuery = false
|
|
continue
|
|
}
|
|
if s.Frame.Score > 0 && s.Frame.Score < 0.6 {
|
|
needsClarify = true
|
|
}
|
|
if isWriteIntent(s.Frame.LegacyIntent) || s.Frame.Goal == GoalGenerate || s.Frame.Goal == GoalMutate || s.Frame.Goal == GoalControl {
|
|
needsConfirm = true
|
|
allQuery = false
|
|
}
|
|
if s.Frame.Goal != GoalQuery && s.Frame.Goal != "" {
|
|
allQuery = false
|
|
}
|
|
if s.Frame.ActionKey != "" {
|
|
seenKeys[s.Frame.ActionKey]++
|
|
if seenKeys[s.Frame.ActionKey] > 1 {
|
|
needsClarify = true
|
|
}
|
|
}
|
|
}
|
|
if needsClarify {
|
|
return ComboNeedsClarify
|
|
}
|
|
if needsConfirm {
|
|
return ComboNeedsConfirm
|
|
}
|
|
if allQuery {
|
|
return ComboReadOnly
|
|
}
|
|
if primary != nil && primary.RequiresClarifying {
|
|
return ComboNeedsClarify
|
|
}
|
|
return ComboNeedsConfirm
|
|
}
|
|
|
|
func comboEvidence(p *PlanProposal) []IntentEvidence {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
ev := []IntentEvidence{{Kind: "segmenter", Detail: SegmenterVersion, Weight: 1}}
|
|
for _, s := range p.Segments {
|
|
if s.Evidence != "" {
|
|
ev = append(ev, IntentEvidence{Kind: "split", Detail: s.Evidence, Weight: 0.5})
|
|
}
|
|
}
|
|
return ev
|
|
}
|
|
|
|
func rejectedProposal(input string, hint ComboHint, reason string) *PlanProposal {
|
|
_ = input
|
|
p := &PlanProposal{
|
|
SegmenterVersion: SegmenterVersion,
|
|
ComboHint: hint,
|
|
Evidence: []IntentEvidence{{Kind: reason, Detail: string(hint), Weight: 1}},
|
|
Segments: []ProposedSegment{{
|
|
StepID: "1",
|
|
Text: input,
|
|
Normalized: normalizeSegmentText(input),
|
|
Span: TextSpan{Start: 0, End: len(input)},
|
|
StructureConfidence: "high",
|
|
Evidence: reason,
|
|
}},
|
|
}
|
|
p.Digest = proposalDigest(p)
|
|
return p
|
|
}
|
|
|
|
func proposalDigest(p *PlanProposal) string {
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
h := sha256.New()
|
|
_, _ = h.Write([]byte(p.SegmenterVersion))
|
|
_, _ = h.Write([]byte{0})
|
|
_, _ = h.Write([]byte(p.ComboHint))
|
|
for _, s := range p.Segments {
|
|
_, _ = h.Write([]byte{0})
|
|
_, _ = h.Write([]byte(s.Normalized))
|
|
_, _ = h.Write([]byte{0})
|
|
if s.Frame != nil {
|
|
_, _ = h.Write([]byte(s.Frame.ActionKey))
|
|
}
|
|
for _, a := range s.UnresolvedAnaphora {
|
|
_, _ = h.Write([]byte{1})
|
|
_, _ = h.Write([]byte(a))
|
|
}
|
|
}
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func buildPlanShadow(res *IntentRecognitionResult, p *PlanProposal) *PlanShadowReport {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
dropped := false
|
|
if p.ComboHint == ComboNeedsConfirm || hasWriteSegment(p) {
|
|
dropped = res.IntentPlan == nil
|
|
}
|
|
return &PlanShadowReport{
|
|
Enabled: true,
|
|
SegmenterVersion: p.SegmenterVersion,
|
|
SegmentCount: len(p.Segments),
|
|
Digest: p.Digest,
|
|
ComboHint: p.ComboHint,
|
|
LegacyPlanAttached: res.IntentPlan != nil,
|
|
LegacyDroppedWrite: dropped && hasWriteSegment(p),
|
|
CounterfactualAutoRead: counterfactualAutoRead(p),
|
|
}
|
|
}
|
|
|
|
func hasWriteSegment(p *PlanProposal) bool {
|
|
if p == nil {
|
|
return false
|
|
}
|
|
for _, s := range p.Segments {
|
|
if s.Frame == nil {
|
|
continue
|
|
}
|
|
if isWriteIntent(s.Frame.LegacyIntent) || s.Frame.Goal == GoalGenerate || s.Frame.Goal == GoalMutate || s.Frame.Goal == GoalControl {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CounterfactualAutoRead answers "if the system were in auto_read, what would it do?"
|
|
// It must not treat "did not execute because shadow" as zero error.
|
|
func counterfactualAutoRead(p *PlanProposal) string {
|
|
if p == nil {
|
|
return "would_clarify"
|
|
}
|
|
switch p.ComboHint {
|
|
case ComboReadOnly:
|
|
return "would_execute"
|
|
case ComboSingle:
|
|
if hasWriteSegment(p) {
|
|
return "would_confirm"
|
|
}
|
|
return "would_passthrough"
|
|
case ComboNeedsConfirm:
|
|
return "would_confirm"
|
|
case ComboNeedsClarify:
|
|
return "would_clarify"
|
|
case ComboRejected, ComboRejectedBatchSize:
|
|
return "would_reject"
|
|
default:
|
|
return "would_clarify"
|
|
}
|
|
}
|
|
|
|
func uniqueStrings(in []string) []string {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
seen := map[string]struct{}{}
|
|
var out []string
|
|
for _, s := range in {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[s]; ok {
|
|
continue
|
|
}
|
|
seen[s] = struct{}{}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|