Skip to content

Commit 708df5f

Browse files
fix(researcher): infer intent for JIT selectors
1 parent a1d89ab commit 708df5f

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

‎internal/core/intent_inference.go‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package core
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strings"
7+
"time"
8+
)
9+
10+
// InferIntentFromTask derives a best-effort StructuredIntent from a free-form task string.
11+
//
12+
// This is primarily used for shard types that receive an unstructured "task" (e.g., user-defined
13+
// specialist agents backed by the researcher base shard) so that JIT prompt selectors like
14+
// intent_verbs can behave deterministically.
15+
func InferIntentFromTask(task string) *StructuredIntent {
16+
trimmed := strings.TrimSpace(task)
17+
verb, remainder := inferVerbAndRemainder(trimmed)
18+
19+
constraint := strings.TrimSpace(remainder)
20+
if constraint == "" {
21+
constraint = strings.TrimSpace(task)
22+
}
23+
24+
intent := &StructuredIntent{
25+
ID: fmt.Sprintf("inferred-intent-%d", time.Now().UnixNano()),
26+
Category: inferCategoryFromVerb(verb),
27+
Verb: verb,
28+
Target: inferTargetFromText(constraint),
29+
Constraint: constraint,
30+
}
31+
32+
return intent
33+
}
34+
35+
func inferVerbAndRemainder(task string) (string, string) {
36+
if task == "" {
37+
return "/explain", ""
38+
}
39+
40+
// Allow an explicit verb token at the start of the task: "/debug ...", "/explain ...", etc.
41+
if strings.HasPrefix(task, "/") {
42+
first, rest := splitFirstToken(task)
43+
if isSupportedVerb(first) {
44+
return first, strings.TrimSpace(rest)
45+
}
46+
}
47+
48+
firstWord, rest := splitFirstToken(task)
49+
switch strings.ToLower(firstWord) {
50+
case "explain", "describe", "summarize", "overview", "teach":
51+
return "/explain", rest
52+
case "debug", "investigate", "triage", "diagnose":
53+
return "/debug", rest
54+
case "fix", "repair", "resolve":
55+
return "/fix", rest
56+
case "refactor", "rewrite", "cleanup", "restructure":
57+
return "/refactor", rest
58+
case "test", "tests", "pytest", "gotest":
59+
return "/test", rest
60+
case "review", "audit":
61+
return "/review", rest
62+
case "security", "secure", "harden":
63+
return "/security", rest
64+
case "research", "find", "lookup":
65+
return "/research", rest
66+
case "create", "build", "implement", "add":
67+
return "/create", rest
68+
default:
69+
// Default to /explain to ensure intent-based selectors can safely block non-matching atoms.
70+
return "/explain", task
71+
}
72+
}
73+
74+
func splitFirstToken(s string) (string, string) {
75+
s = strings.TrimSpace(s)
76+
if s == "" {
77+
return "", ""
78+
}
79+
for i, r := range s {
80+
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
81+
return s[:i], strings.TrimSpace(s[i:])
82+
}
83+
}
84+
return s, ""
85+
}
86+
87+
func isSupportedVerb(v string) bool {
88+
switch strings.TrimSpace(v) {
89+
case "/fix", "/debug", "/refactor", "/test", "/review", "/security", "/research", "/explain", "/create":
90+
return true
91+
default:
92+
return false
93+
}
94+
}
95+
96+
func inferCategoryFromVerb(verb string) string {
97+
switch verb {
98+
case "/fix", "/debug", "/refactor", "/test", "/create":
99+
return "/mutation"
100+
case "/review", "/security", "/research", "/explain":
101+
return "/query"
102+
default:
103+
return "/query"
104+
}
105+
}
106+
107+
func inferTargetFromText(text string) string {
108+
for _, tok := range strings.Fields(text) {
109+
tok = strings.Trim(tok, " \t\r\n,;:()[]{}<>\"'`")
110+
if tok == "" {
111+
continue
112+
}
113+
ext := strings.ToLower(filepath.Ext(tok))
114+
switch ext {
115+
case ".go", ".rs", ".py", ".ts", ".tsx", ".js", ".jsx", ".java", ".md", ".yaml", ".yml", ".mg", ".gl":
116+
return tok
117+
}
118+
}
119+
return ""
120+
}

‎internal/shards/researcher/researcher.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,14 @@ func (r *ResearcherShard) buildSystemPrompt(ctx context.Context, task string) (s
886886
ShardType: "researcher",
887887
SessionCtx: sessionCtx,
888888
}
889+
// Ensure we always provide an intent so JIT selectors (intent_verbs) can block
890+
// non-matching atoms deterministically. When no SessionContext is present (common
891+
// for user-defined specialists spawned from the TUI), infer the intent from the task.
892+
if sessionCtx != nil && sessionCtx.UserIntent != nil {
893+
promptCtx.UserIntent = sessionCtx.UserIntent
894+
} else {
895+
promptCtx.UserIntent = core.InferIntentFromTask(task)
896+
}
889897

890898
systemPrompt, err := assembler.AssembleSystemPrompt(ctx, promptCtx)
891899
if err != nil {

0 commit comments

Comments
 (0)