Files
Agent-System/internal/intent/proposal.go
T
weichenandCursor 932ba87daf feat(intent): add rule segmenter and shadow PlanProposal
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>
2026-09-15 23:14:29 +00:00

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
}