返回 DeepSeek-Reasonix
goal_budget_heuristic.go
根目录 / internal / control / goal_budget_heuristic.go
1 package control
2
3 import (
4 "slices"
5 "strings"
6 "unicode/utf8"
7 )
8
9 // heuristicInputIsTask reports whether a user input reads as an actionable
10 // task rather than conversational chat. The delivery evidence gate uses it to
11 // decide when a turn should be held to acceptance-criteria expectations
12 // (NeedsEvidence); greetings and acknowledgements must not arm the
13 // delivery gates.
14 func heuristicInputIsTask(input string) bool {
15 trimmed := strings.TrimSpace(input)
16 if trimmed == "" {
17 return false
18 }
19
20 normalized := strings.ToLower(strings.Trim(trimmed, " \t\r\n.!?。!?,,;;::"))
21
22 // Very short greeting/acknowledgement whitelist (1-3 words).
23 shortGreetings := []string{
24 "hello", "hi", "hey", "你好", "您好", "nihao",
25 "thanks", "thank you", "谢谢", "谢了",
26 "ok", "okay", "好的", "嗯", "行",
27 "got it", "i see", "明白", "了解", "收到", "我知道了", "先不用",
28 }
29
30 words := strings.Fields(normalized)
31 if len(words) <= 3 {
32 if slices.Contains(shortGreetings, normalized) {
33 return false
34 }
35 }
36
37 // Polite acknowledgements can contain action words from the completed
38 // task ("thanks for fixing") but should stay conversational.
39 chatPhrases := []string{
40 "thanks for", "thank you for", "i'll check later", "i will check later",
41 "i'll test it later", "i will test it later", "that test was helpful", "the test was helpful",
42 "谢谢你", "辛苦了",
43 }
44 for _, phrase := range chatPhrases {
45 before, after, ok := strings.Cut(normalized, phrase)
46 if !ok {
47 continue
48 }
49 // Acknowledgement wording only short-circuits a purely conversational
50 // turn. Preserve a real task before it or after an explicit transition,
51 // e.g. "thanks for fixing that; now update the tests".
52 if prefix := strings.TrimSpace(before); prefix != "" && heuristicInputHasStrongTaskSignal(prefix) {
53 return true
54 }
55 if deliveryTaskHasFollowUpAfterChat(after) {
56 return true
57 }
58 return false
59 }
60 // Ambiguous prose stays conversational. Length is not a task signal;
61 // mutations, files, commands, failures, and audit verbs are classified below.
62 return heuristicInputHasStrongTaskSignal(normalized)
63 }
64
65 func heuristicInputHasStrongTaskSignal(input string) bool {
66 normalized := strings.ToLower(strings.TrimSpace(input))
67 // File references and concrete commands are strong task signals. Shared
68 // parsing keeps email addresses and remote product names from accidentally
69 // arming the delivery gate while covering ordinary repository file types.
70 if deliveryTaskHasFileReference(normalized) || deliveryTaskHasCommand(normalized) {
71 return true
72 }
73 // Mutation intent has a richer, negation-aware vocabulary than this generic
74 // task heuristic. Reuse it so short requests such as "push the branch" do not
75 // bypass delivery gates merely because the two keyword lists drift apart.
76 if deliveryTaskHasMutationIntent(normalized) || NeedsPersistentAction(normalized) {
77 return true
78 }
79
80 // Failure/help descriptions are actionable even when phrased without an
81 // imperative verb, e.g. "the auth isn't working". Shared fault signals keep
82 // task recognition and Goal budget classification from drifting apart.
83 if taskInputHasFaultSignal(normalized) {
84 return true
85 }
86 helpPhrases := []string{
87 "can you help", "help with", "cannot", "can't",
88 "无法", "不能",
89 }
90 for _, phrase := range helpPhrases {
91 if strings.Contains(normalized, phrase) {
92 return true
93 }
94 }
95
96 // Action keyword detection.
97 actionNeedles := []string{
98 "fix", "debug", "repair", "resolve", "reproduce",
99 "create", "add", "write", "edit", "update", "change", "delete", "remove", "rename",
100 "review", "inspect", "analyze", "check", "audit", "verify", "test", "run", "build", "implement", "refactor", "modify", "patch", "replace",
101 "configure", "upgrade", "downgrade", "enable", "disable", "merge", "make changes", "make a change", "make the changes",
102 "make the requested changes", "make the necessary changes", "make these changes", "make those changes", "make code changes",
103 "continue work", "continue the", "continue this",
104 "修复", "调试", "解决", "复现", "创建", "新建", "添加", "编写", "编辑", "修改", "更新",
105 "删除", "移除", "重命名", "评审", "检查", "分析", "审计", "验证", "测试", "运行", "构建", "实现", "重构", "继续处理",
106 "调整", "替换", "移动", "升级", "降级", "启用", "禁用", "合并", "改动", "打补丁",
107 "看看", "看下", "帮我看", "帮我看下", "处理下", "处理一下", "排查", "定位",
108 }
109
110 for _, needle := range actionNeedles {
111 if containsTaskNeedle(normalized, needle) {
112 return true
113 }
114 }
115
116 return false
117 }
118
119 func deliveryTaskHasFollowUpAfterChat(input string) bool {
120 for index, current := range input {
121 switch current {
122 case '.', ',', ';', '!', '?', '。', ',', ';', '!', '?':
123 candidate := strings.TrimSpace(input[index+utf8.RuneLen(current):])
124 if candidate != "" && heuristicInputHasStrongTaskSignal(candidate) {
125 return true
126 }
127 }
128 }
129 for _, cue := range []string{
130 " but ", " however ", " nevertheless ", " now ", " then ", " and ", " please ", " so ", " therefore ",
131 "但是", "但请", "不过", "现在", "然后", "所以", "请", "继续", "再",
132 } {
133 for rest := input; ; {
134 index := strings.Index(rest, cue)
135 if index < 0 {
136 break
137 }
138 candidate := strings.TrimSpace(rest[index+len(cue):])
139 if candidate != "" && heuristicInputHasStrongTaskSignal(candidate) {
140 return true
141 }
142 rest = rest[index+len(cue):]
143 }
144 }
145 return false
146 }
147
148 func containsTaskNeedle(input, needle string) bool {
149 if needle == "" {
150 return false
151 }
152 if goalBudgetContainsNonASCII(needle) || strings.Contains(needle, " ") {
153 return strings.Contains(input, needle)
154 }
155 return slices.Contains(strings.FieldsFunc(input, func(r rune) bool {
156 return !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '_'
157 }), needle)
158 }
159
160 func goalBudgetContainsNonASCII(s string) bool {
161 for _, r := range s {
162 if r > 127 {
163 return true
164 }
165 }
166 return false
167 }
168
168 lines GO