返回 DeepSeek-Reasonix
planner_gate.go
根目录 / internal / control / planner_gate.go
1 package control
2
3 import (
4 "context"
5 "regexp"
6 "strings"
7 "unicode"
8 "unicode/utf8"
9
10 "reasonix/internal/agent"
11 "reasonix/internal/capability"
12 )
13
14 const (
15 plannerLightResearchRounds = 2
16 plannerFullResearchRounds = 6
17 )
18
19 const (
20 plannerReasonExplicitPlanMode = "explicit_plan_mode"
21 plannerReasonSynthetic = "synthetic"
22 plannerReasonSlash = "slash_command"
23 plannerReasonShortReply = "short_reply"
24 plannerReasonConversation = "conversation"
25 plannerReasonUserDirect = "user_direct"
26 plannerReasonUserPlanOnly = "user_plan_only"
27 plannerReasonUserPlanApproval = "user_plan_for_approval"
28 plannerReasonUserPlanAndExecute = "user_plan_and_execute"
29 plannerReasonContextContinuation = "context_continuation"
30 plannerReasonLowRiskQuestion = "low_risk_question"
31 plannerReasonHighRisk = "high_risk"
32 plannerReasonCrossSurface = "cross_surface"
33 plannerReasonStructuredRequest = "structured_request"
34 plannerReasonComplexIntent = "complex_intent"
35 plannerReasonAtomicEdit = "atomic_edit"
36 plannerReasonReadOnlyAction = "read_only_action"
37 plannerReasonGuidance = "complex_guidance"
38 plannerReasonGoalActive = "goal_active"
39 plannerReasonAnchoredWork = "anchored_work"
40 plannerReasonAmbiguousWork = "ambiguous_work"
41 plannerReasonWorkRequest = "work_request"
42 plannerReasonDefault = "default_executor"
43 )
44
45 var (
46 directOptionReplyRE = regexp.MustCompile(`(?i)^\s*(?:\d+|[a-z])\s*[.)、。]?\s*$`)
47 prefixedOptionReplyRE = regexp.MustCompile(`(?i)^\s*(?:选|选择|就|用|按|走|执行|choose|pick|use|option|choice|方案)\s*(?:第\s*)?(?:方案|选项|option|choice)?\s*(?:\d+|[一二三四五六七八九十]|[a-z])\s*(?:个|号|项|种|条|方案|option|choice)?\s*[.)、。!!??]?\s*$`)
48 plannerListRE = regexp.MustCompile(`(?m)^\s*(?:[-*]|\d+[.)、])\s+\S`)
49 plannerFileRefRE = regexp.MustCompile(`(?i)(?:^|[\s@` + "`" + `"'(])(?:[\w.-]+[/\\])*[\w.-]+\.(?:go|ts|tsx|js|jsx|py|rs|java|kt|md|json|ya?ml|toml|sql|sh|css|html)(?:$|[\s,;:!?,;:!?)` + "`" + `"'])`)
50 )
51
52 type plannerTurnMetadata struct {
53 UserText string
54 Synthetic bool
55 ExplicitPlanMode bool
56 GoalActive bool
57 DeliveryProfile bool
58 HasConversationContext bool
59 }
60
61 type plannerTurnMetadataKey struct{}
62
63 func withPlannerTurnMetadata(ctx context.Context, meta plannerTurnMetadata) context.Context {
64 return context.WithValue(ctx, plannerTurnMetadataKey{}, meta)
65 }
66
67 func plannerTurnMetadataFromContext(ctx context.Context) (plannerTurnMetadata, bool) {
68 if ctx == nil {
69 return plannerTurnMetadata{}, false
70 }
71 meta, ok := ctx.Value(plannerTurnMetadataKey{}).(plannerTurnMetadata)
72 return meta, ok
73 }
74
75 func (c *Controller) withPlannerTurnMetadata(ctx context.Context, userText string, synthetic bool, priorMessages int) context.Context {
76 return withPlannerTurnMetadata(ctx, plannerTurnMetadata{
77 UserText: userText,
78 Synthetic: synthetic,
79 ExplicitPlanMode: c.PlanMode(),
80 GoalActive: c.goals.active(),
81 DeliveryProfile: c.runtimeProfile == capability.ProfileDelivery,
82 HasConversationContext: priorMessages > 1,
83 })
84 }
85
86 // DecidePlannerRoute applies deterministic precedence rules to a pristine user
87 // turn plus trusted host metadata. It never calls a model and never parses
88 // controller-injected XML to infer host state.
89 func DecidePlannerRoute(ctx context.Context, input string) agent.PlannerDecision {
90 meta, hasMeta := plannerTurnMetadataFromContext(ctx)
91 composedText := strings.TrimSpace(agent.StripTransientUserBlocks(input))
92 text := composedText
93 if hasMeta && strings.TrimSpace(meta.UserText) != "" {
94 text = strings.TrimSpace(meta.UserText)
95 }
96
97 if meta.ExplicitPlanMode || strings.HasPrefix(composedText, PlanModeMarker) {
98 return plannerExecutorDecision(plannerReasonExplicitPlanMode)
99 }
100 if meta.Synthetic || IsSyntheticUserMessage(text) {
101 return plannerExecutorDecision(plannerReasonSynthetic)
102 }
103 if text == "" {
104 return plannerExecutorDecision(plannerReasonConversation)
105 }
106 if strings.HasPrefix(text, "/") {
107 return plannerExecutorDecision(plannerReasonSlash)
108 }
109 if isContextDependentShortReply(text) {
110 return plannerExecutorDecision(plannerReasonShortReply)
111 }
112 if isConversationalTurn(text) {
113 return plannerExecutorDecision(plannerReasonConversation)
114 }
115
116 lower := normalizePlannerText(text)
117 if requestsPlanApproval(lower) {
118 return plannerPlanDecision(agent.PlannerRoutePlanForApproval, agent.PlannerDepthFull, plannerReasonUserPlanApproval)
119 }
120 if requestsPlanOnly(lower) {
121 return plannerPlanDecision(agent.PlannerRoutePlanOnly, agent.PlannerDepthFull, plannerReasonUserPlanOnly)
122 }
123 if hasLeadingDirective(lower, planAndExecuteDirectives) || hasLeadingDirective(lower, planFirstDirectives) {
124 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthFull, plannerReasonUserPlanAndExecute)
125 }
126 if requestsDirectExecution(lower) {
127 return plannerExecutorDecision(plannerReasonUserDirect)
128 }
129 if meta.HasConversationContext && isContextDependentAction(text) {
130 return plannerExecutorDecision(plannerReasonContextContinuation)
131 }
132 if isLowRiskQuestion(lower) {
133 return plannerExecutorDecision(plannerReasonLowRiskQuestion)
134 }
135
136 features := plannerFeaturesFor(text, lower)
137 if features.work && features.highRisk {
138 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthFull, plannerReasonHighRisk)
139 }
140 if features.multiFile || features.crossSurface {
141 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthFull, plannerReasonCrossSurface)
142 }
143 if features.structured {
144 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthFull, plannerReasonStructuredRequest)
145 }
146 if features.complex {
147 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthFull, plannerReasonComplexIntent)
148 }
149 if features.atomic {
150 return plannerExecutorDecision(plannerReasonAtomicEdit)
151 }
152 if features.guidance {
153 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthLight, plannerReasonGuidance)
154 }
155 if features.readOnly && !features.ambiguous {
156 return plannerExecutorDecision(plannerReasonReadOnlyAction)
157 }
158 if meta.GoalActive && features.work {
159 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthFull, plannerReasonGoalActive)
160 }
161 if meta.DeliveryProfile && features.work {
162 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthFull, plannerReasonWorkRequest)
163 }
164 if features.work && features.ambiguous {
165 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthFull, plannerReasonAmbiguousWork)
166 }
167 if features.work && features.anchored {
168 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthLight, plannerReasonAnchoredWork)
169 }
170 if features.work {
171 return plannerPlanDecision(agent.PlannerRoutePlanAndExecute, agent.PlannerDepthLight, plannerReasonWorkRequest)
172 }
173 return plannerExecutorDecision(plannerReasonDefault)
174 }
175
176 func plannerExecutorDecision(reason string) agent.PlannerDecision {
177 return agent.PlannerDecision{
178 Route: agent.PlannerRouteExecutorOnly,
179 Depth: agent.PlannerDepthNone,
180 Reason: reason,
181 }
182 }
183
184 func plannerPlanDecision(route agent.PlannerRoute, depth agent.PlannerDepth, reason string) agent.PlannerDecision {
185 rounds := plannerLightResearchRounds
186 if depth == agent.PlannerDepthFull {
187 rounds = plannerFullResearchRounds
188 }
189 return agent.PlannerDecision{
190 Route: route,
191 Depth: depth,
192 Reason: reason,
193 MaxResearchRounds: rounds,
194 }
195 }
196
197 type plannerFeatures struct {
198 work bool
199 highRisk bool
200 multiFile bool
201 crossSurface bool
202 structured bool
203 complex bool
204 atomic bool
205 readOnly bool
206 guidance bool
207 anchored bool
208 ambiguous bool
209 }
210
211 func plannerFeaturesFor(text, lower string) plannerFeatures {
212 fileRefs := plannerFileRefRE.FindAllString(text, -1)
213 anchored := len(fileRefs) > 0 || strings.Contains(text, "@") || containsAnyLexical(lower, plannerNamedTargets)
214 work := containsAnyLexical(lower, plannerWorkTerms)
215 highRisk := containsAnyLexical(lower, plannerHighRiskTerms)
216 multiFile := len(fileRefs) >= 2 || strings.Count(text, "@") >= 2
217 crossSurface := containsAnyLexical(lower, plannerCrossSurfaceTerms)
218 structured := utf8.RuneCountInString(text) >= 240 || plannerListRE.MatchString(text) || strings.Count(text, "\n") >= 2
219 complex := containsAnyLexical(lower, complexIntentTerms)
220 guidance := isComplexGuidanceQuestion(lower)
221 ambiguous := work && containsAnyLexical(lower, plannerAmbiguousScopeTerms)
222 readOnly := work && containsAnyLexical(lower, plannerReadOnlyWorkTerms) &&
223 !containsAnyLexical(lower, plannerMutationWorkTerms)
224 atomic := work && anchored && !highRisk && !multiFile && !crossSurface && !structured && !complex &&
225 utf8.RuneCountInString(text) <= 140 && containsAnyLexical(lower, plannerAtomicTerms)
226 return plannerFeatures{
227 work: work,
228 highRisk: highRisk,
229 multiFile: multiFile,
230 crossSurface: crossSurface,
231 structured: structured,
232 complex: complex,
233 atomic: atomic,
234 readOnly: readOnly,
235 guidance: guidance,
236 anchored: anchored,
237 ambiguous: ambiguous,
238 }
239 }
240
241 func normalizePlannerText(text string) string {
242 text = strings.ToLower(strings.TrimSpace(text))
243 text = strings.ReplaceAll(text, "’", "'")
244 return text
245 }
246
247 func hasLeadingDirective(lower string, directives []string) bool {
248 lower = strings.TrimSpace(lower)
249 for _, polite := range []string{"please ", "please, ", "请", "请先", "麻烦", "麻烦先"} {
250 if strings.HasPrefix(lower, polite) {
251 lower = strings.TrimSpace(strings.TrimPrefix(lower, polite))
252 break
253 }
254 }
255 for _, directive := range directives {
256 if strings.HasPrefix(lower, directive) {
257 return true
258 }
259 }
260 return false
261 }
262
263 var planAndExecuteDirectives = []string{
264 "先规划再执行", "先规划再实现", "先出方案再执行", "先出方案再实现",
265 "plan first, then", "plan first then", "plan then implement", "plan and implement",
266 }
267
268 var planFirstDirectives = []string{
269 "先规划", "先给方案", "先出方案",
270 "plan first", "draft a plan", "give me a plan", "make a plan",
271 }
272
273 var planOnlyDirectives = []string{
274 "只规划", "只做规划", "只给方案", "只出方案", "给我方案即可",
275 "plan only", "only plan", "just plan", "give me only a plan", "give me a plan only",
276 }
277
278 var planOnlyBoundaryTerms = []string{
279 "give me only a plan", "give me a plan only", "only give me the plan",
280 "给我方案即可", "只要方案",
281 }
282
283 var plannerNoExecutionTerms = []string{
284 "不要执行", "先别执行", "暂不执行", "不要实现", "先别实现", "暂不实现",
285 "不要修改", "先别修改", "不要改代码", "先别改代码", "不要动代码",
286 "do not execute", "don't execute", "do not implement", "don't implement",
287 "do not make changes", "don't make changes", "without executing",
288 "without implementation", "no execution", "no implementation",
289 }
290
291 var plannerApprovalTerms = []string{
292 "等我确认", "等待我确认", "我确认后", "确认后再",
293 "等我批准", "等待我批准", "我批准后", "批准后再",
294 "wait for my approval", "wait for approval", "after i approve", "after my approval",
295 "until i approve", "until my approval", "let me approve", "let me confirm",
296 "after i confirm", "after my confirmation",
297 }
298
299 var directExecutionDirectives = []string{
300 "直接改", "直接修改", "直接做", "直接执行", "别规划", "不要规划", "无需规划",
301 "just do it", "skip the plan",
302 }
303
304 func requestsPlanOnly(lower string) bool {
305 directiveText := plannerDirectiveText(lower)
306 if hasLeadingDirective(directiveText, planOnlyDirectives) {
307 return true
308 }
309 if containsAnyLexical(directiveText, planOnlyBoundaryTerms) {
310 return true
311 }
312 if (strings.Contains(directiveText, "只给") || strings.Contains(directiveText, "只要")) &&
313 containsAnyLexical(directiveText, plannerIntentTerms) {
314 return true
315 }
316 return containsAnyLexical(directiveText, plannerNoExecutionTerms) &&
317 (containsAnyLexical(directiveText, plannerIntentTerms) ||
318 containsAnyLexical(directiveText, plannerWorkTerms))
319 }
320
321 func requestsPlanApproval(lower string) bool {
322 directiveText := plannerDirectiveText(lower)
323 return (containsAnyLexical(directiveText, plannerIntentTerms) ||
324 containsAnyLexical(directiveText, plannerWorkTerms)) &&
325 containsUnnegatedPlannerApproval(directiveText)
326 }
327
328 func requestsDirectExecution(lower string) bool {
329 directiveText := plannerDirectiveText(lower)
330 if containsAnyLexical(directiveText, directExecutionDirectives) {
331 return true
332 }
333 for _, term := range []string{"don't plan", "do not plan"} {
334 offset := 0
335 for offset < len(directiveText) {
336 idx := strings.Index(directiveText[offset:], term)
337 if idx < 0 {
338 break
339 }
340 idx += offset
341 after := strings.TrimSpace(directiveText[idx+len(term):])
342 if !strings.HasPrefix(after, "to ") {
343 return true
344 }
345 offset = idx + len(term)
346 }
347 }
348 return false
349 }
350
351 var plannerIntentTerms = []string{
352 "plan", "planning", "方案", "规划", "计划",
353 }
354
355 func containsUnnegatedPlannerApproval(text string) bool {
356 for _, term := range plannerApprovalTerms {
357 offset := 0
358 for offset < len(text) {
359 idx := strings.Index(text[offset:], term)
360 if idx < 0 {
361 break
362 }
363 idx += offset
364 if !plannerApprovalNegated(text[:idx]) {
365 return true
366 }
367 offset = idx + len(term)
368 }
369 }
370 return false
371 }
372
373 func plannerApprovalNegated(prefix string) bool {
374 prefix = strings.TrimSpace(prefix)
375 for _, negation := range []string{
376 "不要", "不需要", "无需", "无须", "不用", "不必", "别",
377 "do not", "don't", "not", "no need to", "do not need to", "don't need to",
378 "not necessary to", "without",
379 } {
380 if strings.HasSuffix(prefix, negation) {
381 return true
382 }
383 }
384 return false
385 }
386
387 // plannerDirectiveText removes quoted examples before applying execution
388 // boundaries. A user explaining "do not execute" or “别规划” is not issuing
389 // that directive. ASCII apostrophes inside words remain literal, so
390 // contractions such as don't keep matching the directive tables.
391 func plannerDirectiveText(text string) string {
392 var b strings.Builder
393 var closing rune
394 escaped := false
395 runes := []rune(text)
396 for i, r := range runes {
397 if closing != 0 {
398 if escaped {
399 escaped = false
400 b.WriteRune(' ')
401 continue
402 }
403 if (closing == '"' || closing == '`') && r == '\\' {
404 escaped = true
405 b.WriteRune(' ')
406 continue
407 }
408 if r == closing && (closing != '\'' || !plannerInlineApostrophe(runes, i)) {
409 closing = 0
410 }
411 b.WriteRune(' ')
412 continue
413 }
414 switch r {
415 case '"':
416 closing = '"'
417 b.WriteRune(' ')
418 case '“':
419 closing = '”'
420 b.WriteRune(' ')
421 case '‘':
422 // normalizePlannerText converts the closing ’ to ASCII '.
423 closing = '\''
424 b.WriteRune(' ')
425 case '\'':
426 if plannerSingleQuoteStart(runes, i) {
427 closing = '\''
428 b.WriteRune(' ')
429 continue
430 }
431 b.WriteRune(r)
432 case '`':
433 closing = '`'
434 b.WriteRune(' ')
435 default:
436 b.WriteRune(r)
437 }
438 }
439 return b.String()
440 }
441
442 func plannerSingleQuoteStart(runes []rune, i int) bool {
443 if i+1 >= len(runes) || !unicode.IsLetter(runes[i+1]) {
444 return false
445 }
446 return i == 0 || !unicode.IsLetter(runes[i-1]) && !unicode.IsDigit(runes[i-1])
447 }
448
449 func plannerInlineApostrophe(runes []rune, i int) bool {
450 return i > 0 && i+1 < len(runes) &&
451 (unicode.IsLetter(runes[i-1]) || unicode.IsDigit(runes[i-1])) &&
452 (unicode.IsLetter(runes[i+1]) || unicode.IsDigit(runes[i+1]))
453 }
454
455 func isContextDependentAction(text string) bool {
456 text = strings.TrimSpace(text)
457 if text == "" || strings.ContainsAny(text, "\n\r") || utf8.RuneCountInString(text) > 48 {
458 return false
459 }
460 if plannerFileRefRE.MatchString(text) || strings.Contains(text, "@") {
461 return false
462 }
463 lower := normalizePlannerText(text)
464 for _, prefix := range []string{
465 "fix it", "fix this", "do it", "apply it", "make that change", "go ahead with it",
466 "修一下", "改一下", "按这个改", "照这个做", "执行这个", "就这么改", "修复这个问题",
467 } {
468 if strings.HasPrefix(lower, prefix) {
469 return true
470 }
471 }
472 return false
473 }
474
475 func isConversationalTurn(text string) bool {
476 normalized := strings.Trim(strings.ToLower(strings.TrimSpace(text)), " \t\r\n.!?。!?,,;;::")
477 return conversationalTurns[normalized]
478 }
479
480 var conversationalTurns = map[string]bool{
481 "hello": true, "hi": true, "hey": true, "thanks": true, "thank you": true,
482 "你好": true, "您好": true, "谢谢": true, "辛苦了": true, "收到": true, "明白": true,
483 }
484
485 func isContextDependentShortReply(text string) bool {
486 text = strings.TrimSpace(text)
487 if text == "" || strings.ContainsAny(text, "\n\r") {
488 return false
489 }
490 if directOptionReplyRE.MatchString(text) || prefixedOptionReplyRE.MatchString(text) {
491 return true
492 }
493 lower := strings.ToLower(text)
494 if containsAnyLexical(lower, complexIntentTerms) || containsAnyLexical(lower, plannerWorkTerms) {
495 return false
496 }
497 if shortContextReplies[lower] {
498 return true
499 }
500 if utf8.RuneCountInString(text) > 16 {
501 return false
502 }
503 for _, prefix := range shortContextReplyPrefixes {
504 if strings.HasPrefix(lower, prefix) {
505 return true
506 }
507 }
508 return false
509 }
510
511 var shortContextReplies = map[string]bool{
512 "ok": true, "okay": true, "yes": true, "y": true, "no": true, "n": true,
513 "sure": true, "go ahead": true, "proceed": true, "continue": true, "next": true,
514 "sounds good": true, "好": true, "好的": true, "可以": true, "行": true,
515 "嗯": true, "对": true, "是": true, "确认": true, "同意": true, "继续": true,
516 "继续吧": true, "下一步": true, "开始": true, "开始吧": true, "执行": true,
517 "就这样": true, "没问题": true,
518 }
519
520 var shortContextReplyPrefixes = []string{
521 "继续", "执行", "开始", "下一步", "go ahead", "proceed", "continue",
522 }
523
524 func isLowRiskQuestion(lower string) bool {
525 lower = strings.TrimSpace(lower)
526 normalized := strings.ReplaceAll(lower, "'", "")
527 if strings.HasPrefix(lower, "what ") || strings.HasPrefix(normalized, "whats ") ||
528 strings.HasPrefix(lower, "why ") || strings.HasPrefix(lower, "how ") ||
529 strings.HasPrefix(lower, "who ") || strings.HasPrefix(lower, "where ") ||
530 strings.HasPrefix(lower, "when ") || strings.HasPrefix(lower, "which ") ||
531 strings.HasPrefix(lower, "whose ") || strings.HasPrefix(lower, "whom ") ||
532 strings.HasPrefix(lower, "explain ") || strings.HasPrefix(lower, "describe ") ||
533 strings.HasPrefix(lower, "tell ") || strings.HasPrefix(lower, "show ") ||
534 strings.HasPrefix(lower, "list ") || strings.HasPrefix(lower, "summarize ") ||
535 strings.HasPrefix(lower, "summarise ") || strings.HasPrefix(lower, "compare ") ||
536 strings.HasPrefix(lower, "difference ") || strings.HasPrefix(lower, "is ") ||
537 strings.HasPrefix(lower, "are ") || strings.HasPrefix(lower, "can ") ||
538 strings.HasPrefix(lower, "could ") || strings.HasPrefix(lower, "do ") ||
539 strings.HasPrefix(lower, "does ") || strings.HasPrefix(lower, "did ") ||
540 strings.HasPrefix(lower, "should ") || strings.HasPrefix(lower, "would ") ||
541 strings.HasPrefix(lower, "will ") ||
542 strings.HasPrefix(lower, "what's") || strings.HasPrefix(normalized, "whats") ||
543 strings.HasPrefix(lower, "解释") || strings.HasPrefix(lower, "说明") ||
544 strings.HasPrefix(lower, "怎么看") || strings.HasPrefix(lower, "查一下") ||
545 strings.HasPrefix(lower, "介绍一下") ||
546 strings.HasPrefix(lower, "说一下") || strings.HasPrefix(lower, "帮我看") ||
547 strings.HasPrefix(lower, "帮我查") || strings.HasPrefix(lower, "是什么") ||
548 strings.HasPrefix(lower, "有没有") || strings.HasPrefix(lower, "能不能") ||
549 strings.HasPrefix(lower, "可以吗") || strings.HasPrefix(lower, "对吗") ||
550 strings.HasPrefix(lower, "是不是") || strings.HasPrefix(lower, "请问") {
551 return !containsAnyLexical(lower, plannerQuestionWorkTerms)
552 }
553 return false
554 }
555
556 func isComplexGuidanceQuestion(lower string) bool {
557 for _, prefix := range []string{
558 "how do i ", "how should i ", "how would you ", "what's the best way ",
559 "what is the best way ", "explain how to ", "怎么实现", "如何实现", "怎么迁移", "如何迁移",
560 } {
561 if strings.HasPrefix(lower, prefix) {
562 return true
563 }
564 }
565 return false
566 }
567
568 func containsAnyLexical(s string, terms []string) bool {
569 for _, term := range terms {
570 if containsLexicalTerm(s, term) {
571 return true
572 }
573 }
574 return false
575 }
576
577 func containsLexicalTerm(s, term string) bool {
578 term = strings.ToLower(strings.TrimSpace(term))
579 if term == "" {
580 return false
581 }
582 if containsNonASCII(term) || strings.ContainsAny(term, " -_/") {
583 return strings.Contains(s, term)
584 }
585 for _, token := range strings.FieldsFunc(s, func(r rune) bool {
586 return !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_'
587 }) {
588 if token == term {
589 return true
590 }
591 }
592 return false
593 }
594
595 func containsNonASCII(s string) bool {
596 for _, r := range s {
597 if r > unicode.MaxASCII {
598 return true
599 }
600 }
601 return false
602 }
603
604 var complexIntentTerms = []string{
605 "refactor", "migrate", "migration", "redesign", "end-to-end", "e2e", "wire up",
606 "integration", "architecture", "release", "package", "重构", "迁移", "改造",
607 "端到端", "联调", "接入", "架构", "发布", "打包",
608 }
609
610 var plannerWorkTerms = []string{
611 "fix", "fixing", "update", "updating", "remove", "removing", "delete", "deleting",
612 "edit", "editing", "write", "writing", "create", "creating", "add", "adding", "repair",
613 "patch", "run", "running", "build", "building", "implement", "implementing", "refactor",
614 "refactoring", "migrate", "migrating", "redesign", "review", "reviewing", "audit",
615 "inspect", "debug", "test", "tests", "testing", "修改", "修复", "更新", "删除", "移除",
616 "编辑", "写入", "创建", "新增", "添加", "运行", "构建", "实现", "重构", "迁移",
617 "改造", "评审", "审查", "排查", "调试", "测试", "加个", "加一", "补一个", "补个",
618 }
619
620 var plannerMutationWorkTerms = []string{
621 "fix", "fixing", "update", "updating", "remove", "removing", "delete", "deleting",
622 "edit", "editing", "write", "writing", "create", "creating", "add", "adding", "repair",
623 "patch", "build", "building", "implement", "implementing", "refactor", "refactoring",
624 "migrate", "migrating", "redesign", "修改", "修复", "更新", "删除", "移除", "编辑",
625 "写入", "创建", "新增", "添加", "构建", "实现", "重构", "迁移", "改造", "加个",
626 "加一", "补一个", "补个",
627 }
628
629 var plannerReadOnlyWorkTerms = []string{
630 "run", "running", "review", "reviewing", "audit", "inspect", "debug",
631 "test", "tests", "testing", "运行", "评审", "审查", "排查", "调试", "测试",
632 }
633
634 var plannerQuestionWorkTerms = []string{
635 "fix", "fixing", "update", "updating", "remove", "removing", "delete", "deleting",
636 "edit", "editing", "write", "writing", "create", "creating", "add", "adding", "repair",
637 "patch", "implement", "implementing", "refactor", "refactoring", "migrate", "migrating",
638 "redesign", "修改", "修复", "更新", "删除", "移除", "编辑", "写入", "创建", "新增",
639 "添加", "实现", "重构", "迁移", "改造", "加个", "加一", "补一个", "补个",
640 }
641
642 var plannerHighRiskTerms = []string{
643 "auth", "authentication", "authorization", "permission", "token", "secret",
644 "credential", "payment", "billing", "race", "concurrency", "deadlock", "transaction",
645 "encryption", "signature", "sandbox", "privilege", "权限", "鉴权", "认证", "令牌",
646 "密钥", "支付", "账单", "并发", "竞态", "竟态", "死锁", "事务", "加密", "签名",
647 "沙箱", "提权",
648 }
649
650 var plannerCrossSurfaceTerms = []string{
651 "multiple files", "several files", "across", "frontend and backend", "backend and frontend",
652 "api and ui", "ui and api", "database and api", "多个文件", "多处", "前后端",
653 "整个模块", "整个项目", "全链路", "跨模块",
654 }
655
656 var plannerAtomicTerms = []string{
657 "typo", "wording", "copy", "readme", "changelog", "nil check", "null check",
658 "log line", "one line", "rename", "文案", "错别字", "拼写", "空指针检查",
659 "nil 检查", "一行日志", "改名", "重命名",
660 }
661
662 var plannerNamedTargets = []string{
663 "readme", "changelog", "makefile", "dockerfile",
664 }
665
666 var plannerAmbiguousScopeTerms = []string{
667 "the bug", "the issue", "the problem", "performance", "everything", "whole module",
668 "这个 bug", "这个bug", "这个问题", "性能", "整个模块", "全部问题",
669 }
670
671 // TaskWarrantsPlanner is retained as a small compatibility predicate for
672 // callers and tests that do not need depth or approval semantics.
673 func TaskWarrantsPlanner(input string) bool {
674 return DecidePlannerRoute(context.Background(), input).Route != agent.PlannerRouteExecutorOnly
675 }
676
677 // NewPlannerPolicy returns the structured deterministic policy used by the
678 // two-model product path.
679 func NewPlannerPolicy() agent.PlannerPolicy {
680 return DecidePlannerRoute
681 }
682
683 // NewPlannerGate retains the historical bool shape for direct callers.
684 func NewPlannerGate() func(context.Context, string) bool {
685 return func(ctx context.Context, input string) bool {
686 return DecidePlannerRoute(ctx, input).Route != agent.PlannerRouteExecutorOnly
687 }
688 }
689
689 lines GO