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