@haoyiyin/workflow 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -10
- package/scripts/postinstall.js +2 -2
- package/src/agents/contracts.ts +559 -0
- package/src/agents/dispatcher-enhanced.ts +350 -0
- package/src/agents/dispatcher.ts +680 -0
- package/src/agents/index.ts +48 -0
- package/src/agents/resilience.ts +255 -0
- package/src/agents/token-budget.ts +83 -0
- package/src/agents/types.ts +73 -0
- package/src/guard/main-agent.ts +245 -0
- package/src/hooks/builtin/index.ts +8 -0
- package/src/hooks/builtin/on-error.ts +23 -0
- package/src/hooks/builtin/post-execute.ts +40 -0
- package/src/hooks/builtin/post-plan.ts +23 -0
- package/src/hooks/builtin/pre-execute.ts +30 -0
- package/src/hooks/builtin/pre-plan.ts +26 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/loader.ts +98 -0
- package/src/hooks/manager.ts +99 -0
- package/src/hooks/types-enhanced.ts +38 -0
- package/src/hooks/types.ts +35 -0
- package/src/index.ts +127 -0
- package/src/persistence/index.ts +17 -0
- package/src/persistence/plan-md.ts +141 -0
- package/src/persistence/state-md.ts +167 -0
- package/src/persistence/types.ts +89 -0
- package/src/router/classifier.ts +610 -0
- package/src/router/guard.ts +483 -0
- package/src/router/index.ts +22 -0
- package/src/router/router.ts +108 -0
- package/src/router/types.ts +127 -0
- package/src/skills/agents-md/SKILL.md +45 -0
- package/src/skills/agents-md/index.ts +33 -0
- package/src/skills/execute-plan/SKILL.md +60 -0
- package/src/skills/execute-plan/index.ts +970 -0
- package/src/skills/index.ts +13 -0
- package/src/skills/quick-task/SKILL.md +54 -0
- package/src/skills/quick-task/index.ts +346 -0
- package/src/skills/registry.ts +59 -0
- package/src/skills/review-diff/SKILL.md +53 -0
- package/src/skills/review-diff/index.ts +394 -0
- package/src/skills/skill.ts +59 -0
- package/src/skills/systematic-debugging/SKILL.md +56 -0
- package/src/skills/systematic-debugging/index.ts +404 -0
- package/src/skills/tdd/SKILL.md +52 -0
- package/src/skills/tdd/index.ts +409 -0
- package/src/skills/to-plan/SKILL.md +56 -0
- package/src/skills/to-plan/index-enhanced.ts +551 -0
- package/src/skills/to-plan/index.ts +586 -0
- package/src/skills/types.ts +47 -0
- package/src/state/cleanup.ts +118 -0
- package/src/state/index.ts +8 -0
- package/src/state/manager.ts +96 -0
- package/src/state/persistence.ts +77 -0
- package/src/state/types.ts +30 -0
- package/src/state/validator.ts +78 -0
- package/src/types.ts +102 -0
- package/src/utils/compress.ts +347 -0
- package/src/utils/git.ts +82 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +23 -0
- package/src/utils/paths.ts +55 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Classifier - determines what type of subagent should handle a request.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes user request text and contextual signals to produce an
|
|
5
|
+
* IntentClassification, then builds a RoutingDecision with the appropriate
|
|
6
|
+
* SubagentConfig and SubagentContract.
|
|
7
|
+
*
|
|
8
|
+
* Classification follows a priority-ordered chain:
|
|
9
|
+
* 1. execute (plan file exists / mid-execution)
|
|
10
|
+
* 2. quick-task (trivial typo/whitespace, small scoped change)
|
|
11
|
+
* 3. review (code review request)
|
|
12
|
+
* 4. debug (root cause analysis)
|
|
13
|
+
* 5. tdd (test-driven development request)
|
|
14
|
+
* 6. plan (new feature / significant change)
|
|
15
|
+
* 7. general (catch-all)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { SubagentConfig } from '../agents/types.js'
|
|
19
|
+
import type {
|
|
20
|
+
IntentType,
|
|
21
|
+
IntentClassification,
|
|
22
|
+
RoutingDecision,
|
|
23
|
+
SubagentContract,
|
|
24
|
+
PermissionSet,
|
|
25
|
+
RouterConfig,
|
|
26
|
+
} from './types.js'
|
|
27
|
+
import { ROUTE_KEYWORDS } from './types.js'
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Classification context
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** Context signals used alongside the raw request text. */
|
|
34
|
+
export interface ClassificationContext {
|
|
35
|
+
/** Whether a plan file already exists for this task/feature */
|
|
36
|
+
planFileExists: boolean
|
|
37
|
+
/** Whether the user is in the middle of executing a plan */
|
|
38
|
+
isExecutingPlan: boolean
|
|
39
|
+
/** Whether there are uncommitted changes in the working tree */
|
|
40
|
+
hasUncommittedChanges: boolean
|
|
41
|
+
/** The current branch name (if available) */
|
|
42
|
+
currentBranch?: string
|
|
43
|
+
/** Any files the user explicitly mentioned */
|
|
44
|
+
mentionedFiles: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Extended classification result with diagnostic metadata. */
|
|
48
|
+
export interface ClassifierResult {
|
|
49
|
+
/** The intent classification */
|
|
50
|
+
classification: IntentClassification
|
|
51
|
+
/** The full routing decision (contract + config) */
|
|
52
|
+
routing: RoutingDecision
|
|
53
|
+
/** Confidence score 0-1 */
|
|
54
|
+
confidence: number
|
|
55
|
+
/** Which rule matched */
|
|
56
|
+
matchedRule: string
|
|
57
|
+
/** Alternative classifications that were considered */
|
|
58
|
+
alternatives: Array<{ type: IntentType; confidence: number }>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Constants
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/** Token budget recommendations by intent type (in thousands). */
|
|
66
|
+
const TOKEN_BUDGETS: Record<IntentType, number> = {
|
|
67
|
+
plan: 64,
|
|
68
|
+
'quick-task': 32,
|
|
69
|
+
execute: 64,
|
|
70
|
+
review: 32,
|
|
71
|
+
debug: 64,
|
|
72
|
+
tdd: 64,
|
|
73
|
+
general: 32,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Subagent roles mapped from intent types. */
|
|
77
|
+
const SUBAGENT_ROLES: Record<IntentType, SubagentConfig['role']> = {
|
|
78
|
+
plan: 'planner',
|
|
79
|
+
'quick-task': 'implementer',
|
|
80
|
+
execute: 'implementer',
|
|
81
|
+
review: 'reviewer',
|
|
82
|
+
debug: 'debugger',
|
|
83
|
+
tdd: 'implementer',
|
|
84
|
+
general: 'general',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Default permissions by intent type. */
|
|
88
|
+
const SUBAGENT_PERMISSIONS: Record<IntentType, PermissionSet> = {
|
|
89
|
+
plan: {
|
|
90
|
+
readFiles: true,
|
|
91
|
+
searchCode: true,
|
|
92
|
+
runCommands: false,
|
|
93
|
+
writeFiles: true,
|
|
94
|
+
gitOperations: false,
|
|
95
|
+
},
|
|
96
|
+
'quick-task': {
|
|
97
|
+
readFiles: true,
|
|
98
|
+
searchCode: true,
|
|
99
|
+
runCommands: true,
|
|
100
|
+
writeFiles: true,
|
|
101
|
+
gitOperations: false,
|
|
102
|
+
},
|
|
103
|
+
execute: {
|
|
104
|
+
readFiles: true,
|
|
105
|
+
searchCode: true,
|
|
106
|
+
runCommands: true,
|
|
107
|
+
writeFiles: true,
|
|
108
|
+
gitOperations: true,
|
|
109
|
+
},
|
|
110
|
+
review: {
|
|
111
|
+
readFiles: true,
|
|
112
|
+
searchCode: true,
|
|
113
|
+
runCommands: false,
|
|
114
|
+
writeFiles: false,
|
|
115
|
+
gitOperations: false,
|
|
116
|
+
},
|
|
117
|
+
debug: {
|
|
118
|
+
readFiles: true,
|
|
119
|
+
searchCode: true,
|
|
120
|
+
runCommands: true,
|
|
121
|
+
writeFiles: false,
|
|
122
|
+
gitOperations: false,
|
|
123
|
+
},
|
|
124
|
+
tdd: {
|
|
125
|
+
readFiles: true,
|
|
126
|
+
searchCode: true,
|
|
127
|
+
runCommands: true,
|
|
128
|
+
writeFiles: true,
|
|
129
|
+
gitOperations: false,
|
|
130
|
+
},
|
|
131
|
+
general: {
|
|
132
|
+
readFiles: true,
|
|
133
|
+
searchCode: true,
|
|
134
|
+
runCommands: false,
|
|
135
|
+
writeFiles: false,
|
|
136
|
+
gitOperations: false,
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Whether a given intent type uses worktree isolation. */
|
|
141
|
+
const SUBAGENT_ISOLATION: Partial<Record<IntentType, 'worktree'>> = {
|
|
142
|
+
execute: 'worktree',
|
|
143
|
+
plan: 'worktree',
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Decision builders
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Build an IntentClassification from type and metadata.
|
|
152
|
+
*/
|
|
153
|
+
function buildClassification(
|
|
154
|
+
type: IntentType,
|
|
155
|
+
confidence: number,
|
|
156
|
+
reasoning: string
|
|
157
|
+
): IntentClassification {
|
|
158
|
+
return {
|
|
159
|
+
type,
|
|
160
|
+
confidence,
|
|
161
|
+
reasoning,
|
|
162
|
+
suggestedAgent: SUBAGENT_ROLES[type],
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build a RoutingDecision: subagent config + contract + async flag.
|
|
168
|
+
*/
|
|
169
|
+
function buildRouting(
|
|
170
|
+
type: IntentType,
|
|
171
|
+
prompt: string,
|
|
172
|
+
config: RouterConfig,
|
|
173
|
+
owns: string[] = [],
|
|
174
|
+
reads: string[] = []
|
|
175
|
+
): RoutingDecision {
|
|
176
|
+
const subagentConfig: SubagentConfig = {
|
|
177
|
+
role: SUBAGENT_ROLES[type],
|
|
178
|
+
model: config.defaultModel,
|
|
179
|
+
isolation: SUBAGENT_ISOLATION[type],
|
|
180
|
+
tokenBudget: TOKEN_BUDGETS[type] * 1000,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const contract: SubagentContract = {
|
|
184
|
+
permissions: { ...SUBAGENT_PERMISSIONS[type] },
|
|
185
|
+
prompt,
|
|
186
|
+
owns,
|
|
187
|
+
reads,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
subagent: subagentConfig,
|
|
192
|
+
contract,
|
|
193
|
+
async: type !== 'quick-task',
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Pattern matchers
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if request matches a trivial edit (typo, whitespace, single-line fix).
|
|
203
|
+
* The ONE exception to full delegation — quick-task can handle directly.
|
|
204
|
+
*/
|
|
205
|
+
function matchTrivialEdit(
|
|
206
|
+
request: string
|
|
207
|
+
): { type: IntentType; confidence: number; reasoning: string } | null {
|
|
208
|
+
const normalized = request.toLowerCase().trim()
|
|
209
|
+
|
|
210
|
+
const patterns = [
|
|
211
|
+
/^fix\s+(a\s+)?typo\b/i,
|
|
212
|
+
/^fix\s+(a\s+)?spell(ing)?\s+(error|mistake)\b/i,
|
|
213
|
+
/^fix\s+whitespace\b/i,
|
|
214
|
+
/^remove\s+trailing\s+whitespace\b/i,
|
|
215
|
+
/^fix\s+lint(ing)?\s+(error|warning)\b/i,
|
|
216
|
+
/^format\s+(this|a|the)\s+(file|code)\b/i,
|
|
217
|
+
/^(add|remove)\s+(a\s+)?missing\s+semicolon/i,
|
|
218
|
+
/^change\s+(a\s+)?single\s+(character|char|letter)\b/i,
|
|
219
|
+
/^rename\s+(a\s+)?single\s+variable\b/i,
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
for (const p of patterns) {
|
|
223
|
+
if (p.test(normalized)) {
|
|
224
|
+
return {
|
|
225
|
+
type: 'quick-task',
|
|
226
|
+
confidence: 0.85,
|
|
227
|
+
reasoning: 'Trivial single-line fix — route to quick-task',
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Short request referencing a specific line number
|
|
233
|
+
if (normalized.length < 60 && /\bline\s+\d+\b/.test(normalized)) {
|
|
234
|
+
return {
|
|
235
|
+
type: 'quick-task',
|
|
236
|
+
confidence: 0.7,
|
|
237
|
+
reasoning: 'Very short request references a specific line number',
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if request is a code review.
|
|
246
|
+
*/
|
|
247
|
+
function matchReview(
|
|
248
|
+
request: string
|
|
249
|
+
): { type: IntentType; confidence: number; reasoning: string } | null {
|
|
250
|
+
const keywords = ROUTE_KEYWORDS.review
|
|
251
|
+
const normalized = request.toLowerCase().trim()
|
|
252
|
+
|
|
253
|
+
for (const kw of keywords) {
|
|
254
|
+
if (normalized.includes(kw)) {
|
|
255
|
+
return {
|
|
256
|
+
type: 'review',
|
|
257
|
+
confidence: 0.9,
|
|
258
|
+
reasoning: `Request matches review keyword: "${kw}"`,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check if request is about debugging / root cause analysis.
|
|
268
|
+
*/
|
|
269
|
+
function matchDebug(
|
|
270
|
+
request: string
|
|
271
|
+
): { type: IntentType; confidence: number; reasoning: string } | null {
|
|
272
|
+
const keywords = ROUTE_KEYWORDS.debug
|
|
273
|
+
const normalized = request.toLowerCase().trim()
|
|
274
|
+
|
|
275
|
+
for (const kw of keywords) {
|
|
276
|
+
if (normalized.includes(kw)) {
|
|
277
|
+
return {
|
|
278
|
+
type: 'debug',
|
|
279
|
+
confidence: 0.85,
|
|
280
|
+
reasoning: `Request matches debug keyword: "${kw}"`,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Broader debug patterns
|
|
286
|
+
const broadPatterns = [
|
|
287
|
+
/\bwhy\s+(is|does|am\s*i|are)\b.*\b(error|fail|break|crash|wrong|bug)\b/i,
|
|
288
|
+
/\b(stack\s*trace|backtrace)\b/i,
|
|
289
|
+
/\binvestigate\s+(the|a|this|an)\s+(issue|error|bug|crash|problem)\b/i,
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
for (const p of broadPatterns) {
|
|
293
|
+
if (p.test(normalized)) {
|
|
294
|
+
return {
|
|
295
|
+
type: 'debug',
|
|
296
|
+
confidence: 0.75,
|
|
297
|
+
reasoning: 'Request matches broad debug pattern',
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Check if request is about test-driven development.
|
|
307
|
+
*/
|
|
308
|
+
function matchTdd(
|
|
309
|
+
request: string
|
|
310
|
+
): { type: IntentType; confidence: number; reasoning: string } | null {
|
|
311
|
+
const keywords = ROUTE_KEYWORDS.tdd
|
|
312
|
+
const normalized = request.toLowerCase().trim()
|
|
313
|
+
|
|
314
|
+
for (const kw of keywords) {
|
|
315
|
+
if (normalized.includes(kw)) {
|
|
316
|
+
return {
|
|
317
|
+
type: 'tdd',
|
|
318
|
+
confidence: 0.9,
|
|
319
|
+
reasoning: `Request matches TDD keyword: "${kw}"`,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Check if request needs a full plan (complex, multi-step).
|
|
329
|
+
*/
|
|
330
|
+
function matchPlan(
|
|
331
|
+
request: string
|
|
332
|
+
): { type: IntentType; confidence: number; reasoning: string } | null {
|
|
333
|
+
const keywords = ROUTE_KEYWORDS.plan
|
|
334
|
+
const normalized = request.toLowerCase().trim()
|
|
335
|
+
|
|
336
|
+
for (const kw of keywords) {
|
|
337
|
+
if (normalized.includes(kw)) {
|
|
338
|
+
return {
|
|
339
|
+
type: 'plan',
|
|
340
|
+
confidence: 0.85,
|
|
341
|
+
reasoning: `Request matches planning keyword: "${kw}"`,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Additional complexity indicators
|
|
347
|
+
const planIndicators = [
|
|
348
|
+
/\b(complex|major|big|large|significant)\s+(change|refactor|update|feature)\b/i,
|
|
349
|
+
/\b(multi[-\s]?(file|step|phase)|several\s+files?)\b/i,
|
|
350
|
+
/\b(end[-\s]?to[-\s]?end|full[-\s]?stack)\b/i,
|
|
351
|
+
/\b(from\s+scratch|restructure|reorgani[sz]e|rearchitect)\b/i,
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
for (const p of planIndicators) {
|
|
355
|
+
if (p.test(normalized)) {
|
|
356
|
+
return {
|
|
357
|
+
type: 'plan',
|
|
358
|
+
confidence: 0.75,
|
|
359
|
+
reasoning: 'Request contains complexity indicators — planning likely needed',
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Long requests (>500 chars) often need planning
|
|
365
|
+
if (normalized.length > 500) {
|
|
366
|
+
return {
|
|
367
|
+
type: 'plan',
|
|
368
|
+
confidence: 0.55,
|
|
369
|
+
reasoning: 'Long request suggests complex, multi-step work',
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return null
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Check if request is a read-only / informational question.
|
|
378
|
+
*/
|
|
379
|
+
function matchGeneral(
|
|
380
|
+
request: string
|
|
381
|
+
): { type: IntentType; confidence: number; reasoning: string } | null {
|
|
382
|
+
const keywords = ROUTE_KEYWORDS.general
|
|
383
|
+
const normalized = request.toLowerCase().trim()
|
|
384
|
+
|
|
385
|
+
for (const kw of keywords) {
|
|
386
|
+
if (normalized.includes(kw)) {
|
|
387
|
+
return {
|
|
388
|
+
type: 'general',
|
|
389
|
+
confidence: 0.7,
|
|
390
|
+
reasoning: `Request matches general keyword: "${kw}"`,
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return null
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// RequestClassifier
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Analyzes user requests and produces routing decisions.
|
|
404
|
+
*
|
|
405
|
+
* Classification runs through a priority-ordered chain:
|
|
406
|
+
* 1. Context-based: plan exists → 'execute'
|
|
407
|
+
* 2. Pattern-based: trivial-edit → quick-task, review, debug, tdd, plan, general
|
|
408
|
+
* 3. Fallback: 'general'
|
|
409
|
+
*
|
|
410
|
+
* Usage:
|
|
411
|
+
* ```typescript
|
|
412
|
+
* const classifier = new RequestClassifier(config)
|
|
413
|
+
* const result = classifier.classify("Fix the typo in src/utils.ts", {
|
|
414
|
+
* planFileExists: false,
|
|
415
|
+
* isExecutingPlan: false,
|
|
416
|
+
* hasUncommittedChanges: true,
|
|
417
|
+
* mentionedFiles: ['src/utils.ts'],
|
|
418
|
+
* })
|
|
419
|
+
* // result.classification.type === 'quick-task'
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
422
|
+
export class RequestClassifier {
|
|
423
|
+
private readonly config: Readonly<RouterConfig>
|
|
424
|
+
|
|
425
|
+
/** Priority-ordered classification rules. */
|
|
426
|
+
private readonly rules: ReadonlyArray<{
|
|
427
|
+
name: string
|
|
428
|
+
match: (
|
|
429
|
+
request: string,
|
|
430
|
+
ctx: ClassificationContext
|
|
431
|
+
) => { type: IntentType; confidence: number; reasoning: string } | null
|
|
432
|
+
}>
|
|
433
|
+
|
|
434
|
+
constructor(config: RouterConfig) {
|
|
435
|
+
this.config = Object.freeze({ ...config })
|
|
436
|
+
|
|
437
|
+
this.rules = Object.freeze([
|
|
438
|
+
{ name: 'context-execute', match: (_req, ctx) => this.checkContextExecute(ctx) },
|
|
439
|
+
{ name: 'trivial-edit', match: (req, _ctx) => matchTrivialEdit(req) },
|
|
440
|
+
{ name: 'review', match: (req, _ctx) => matchReview(req) },
|
|
441
|
+
{ name: 'debug', match: (req, _ctx) => matchDebug(req) },
|
|
442
|
+
{ name: 'tdd', match: (req, _ctx) => matchTdd(req) },
|
|
443
|
+
{ name: 'plan', match: (req, _ctx) => matchPlan(req) },
|
|
444
|
+
{ name: 'general-keywords', match: (req, _ctx) => matchGeneral(req) },
|
|
445
|
+
])
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* If a plan file exists or we are mid-execution, route to execute.
|
|
450
|
+
*/
|
|
451
|
+
private checkContextExecute(
|
|
452
|
+
ctx: ClassificationContext
|
|
453
|
+
): { type: IntentType; confidence: number; reasoning: string } | null {
|
|
454
|
+
if (ctx.isExecutingPlan) {
|
|
455
|
+
return {
|
|
456
|
+
type: 'execute',
|
|
457
|
+
confidence: 0.95,
|
|
458
|
+
reasoning: 'Currently executing a plan — continue with execute agent',
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (ctx.planFileExists) {
|
|
462
|
+
return {
|
|
463
|
+
type: 'execute',
|
|
464
|
+
confidence: 0.8,
|
|
465
|
+
reasoning: 'A plan file already exists for this task',
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return null
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// -----------------------------------------------------------------------
|
|
472
|
+
// Public API
|
|
473
|
+
// -----------------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Classify a user request and produce a full routing decision.
|
|
477
|
+
*
|
|
478
|
+
* @param request - Raw user request text
|
|
479
|
+
* @param context - Contextual signals to improve accuracy
|
|
480
|
+
* @returns ClassifierResult with classification metadata and routing decision
|
|
481
|
+
*/
|
|
482
|
+
classify(request: string, context: ClassificationContext): ClassifierResult {
|
|
483
|
+
if (!request || request.trim().length === 0) {
|
|
484
|
+
const classification = buildClassification('general', 0.1, 'Empty request')
|
|
485
|
+
const routing = buildRouting('general', request, this.config)
|
|
486
|
+
return {
|
|
487
|
+
classification,
|
|
488
|
+
routing,
|
|
489
|
+
confidence: 0.1,
|
|
490
|
+
matchedRule: 'empty-request',
|
|
491
|
+
alternatives: [],
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Run through the priority-ordered rule chain
|
|
496
|
+
for (const rule of this.rules) {
|
|
497
|
+
try {
|
|
498
|
+
const match = rule.match(request, context)
|
|
499
|
+
if (match) {
|
|
500
|
+
const classification = buildClassification(match.type, match.confidence, match.reasoning)
|
|
501
|
+
const routing = buildRouting(match.type, request, this.config)
|
|
502
|
+
return {
|
|
503
|
+
classification,
|
|
504
|
+
routing,
|
|
505
|
+
confidence: match.confidence,
|
|
506
|
+
matchedRule: rule.name,
|
|
507
|
+
alternatives: this.computeAlternatives(match.type, match.confidence),
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.error(`Classifier rule "${rule.name}" failed:`, error)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Fallback: general
|
|
516
|
+
const classification = buildClassification(
|
|
517
|
+
'general',
|
|
518
|
+
0.3,
|
|
519
|
+
'No specific pattern matched — routing to general agent'
|
|
520
|
+
)
|
|
521
|
+
const routing = buildRouting('general', request, this.config)
|
|
522
|
+
return {
|
|
523
|
+
classification,
|
|
524
|
+
routing,
|
|
525
|
+
confidence: 0.3,
|
|
526
|
+
matchedRule: 'fallback',
|
|
527
|
+
alternatives: [
|
|
528
|
+
{ type: 'plan', confidence: 0.3 },
|
|
529
|
+
{ type: 'quick-task', confidence: 0.2 },
|
|
530
|
+
{ type: 'debug', confidence: 0.2 },
|
|
531
|
+
],
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Classify intent only (without building a full routing decision).
|
|
537
|
+
*/
|
|
538
|
+
classifyIntent(request: string, context: ClassificationContext): IntentClassification {
|
|
539
|
+
return this.classify(request, context).classification
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Produce just the routing decision.
|
|
544
|
+
*/
|
|
545
|
+
route(request: string, context: ClassificationContext): RoutingDecision {
|
|
546
|
+
return this.classify(request, context).routing
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// -----------------------------------------------------------------------
|
|
550
|
+
// Configuration queries
|
|
551
|
+
// -----------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
/** Get recommended token budget for an intent type. */
|
|
554
|
+
getTokenBudget(type: IntentType): number {
|
|
555
|
+
return TOKEN_BUDGETS[type] * 1000
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Get subagent role for an intent type. */
|
|
559
|
+
getSubagentRole(type: IntentType): SubagentConfig['role'] {
|
|
560
|
+
return SUBAGENT_ROLES[type]
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Get default permissions for an intent type (returns a copy). */
|
|
564
|
+
getSubagentPermissions(type: IntentType): PermissionSet {
|
|
565
|
+
return { ...SUBAGENT_PERMISSIONS[type] }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// -----------------------------------------------------------------------
|
|
569
|
+
// Reporting
|
|
570
|
+
// -----------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Generate a human-readable classification summary for logging.
|
|
574
|
+
*/
|
|
575
|
+
describeClassification(result: ClassifierResult): string {
|
|
576
|
+
const { classification, confidence, matchedRule, routing } = result
|
|
577
|
+
return [
|
|
578
|
+
`Intent: ${classification.type}`,
|
|
579
|
+
`Confidence: ${(confidence * 100).toFixed(0)}%`,
|
|
580
|
+
`Rule: ${matchedRule}`,
|
|
581
|
+
`Agent: ${classification.suggestedAgent}`,
|
|
582
|
+
`Reason: ${classification.reasoning}`,
|
|
583
|
+
`Async: ${routing.async}`,
|
|
584
|
+
routing.subagent.isolation ? `Isolation: ${routing.subagent.isolation}` : null,
|
|
585
|
+
]
|
|
586
|
+
.filter(Boolean)
|
|
587
|
+
.join(' | ')
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// -----------------------------------------------------------------------
|
|
591
|
+
// Internal
|
|
592
|
+
// -----------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Compute alternative classifications based on the winning type.
|
|
596
|
+
*/
|
|
597
|
+
private computeAlternatives(
|
|
598
|
+
winningType: IntentType,
|
|
599
|
+
winningConfidence: number
|
|
600
|
+
): Array<{ type: IntentType; confidence: number }> {
|
|
601
|
+
const remaining = 1 - winningConfidence
|
|
602
|
+
const allTypes: IntentType[] = ['plan', 'quick-task', 'execute', 'review', 'debug', 'tdd', 'general']
|
|
603
|
+
const others = allTypes.filter((t) => t !== winningType)
|
|
604
|
+
|
|
605
|
+
return others.slice(0, 3).map((type, i) => ({
|
|
606
|
+
type,
|
|
607
|
+
confidence: Math.round((remaining / (others.length - i)) * 100) / 100,
|
|
608
|
+
}))
|
|
609
|
+
}
|
|
610
|
+
}
|