claude-brain 0.27.2 → 0.28.0

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.
@@ -240,85 +240,94 @@ export class IntentClassifier {
240
240
  // Phase 19 B3: Detect temporal signals for secondary intent
241
241
  const hasTemporal = this.hasTemporalSignal(lower)
242
242
 
243
+ // SLM Phase 1A: Log classification for training data collection
244
+ const startTime = Date.now()
245
+
243
246
  // Check in priority order — first confident match wins
244
247
 
248
+ // Helper to log + return in one step
249
+ const ret = (result: ClassificationResult): ClassificationResult => {
250
+ this._logTraining(message, result, startTime)
251
+ return result
252
+ }
253
+
245
254
  // 1. no_action: very short messages, greetings, acknowledgments
246
255
  if (this.isNoAction(lower)) {
247
- return { primary: 'no_action', confidence: 0.95, secondary: [] }
256
+ return ret({ primary: 'no_action', confidence: 0.95, secondary: [] })
248
257
  }
249
258
 
250
259
  // 2. delete_memory: "forget that", "delete", "remove"
251
260
  if (this.isDeleteMemory(lower)) {
252
- return { primary: 'delete_memory', confidence: 0.90, secondary }
261
+ return ret({ primary: 'delete_memory', confidence: 0.90, secondary })
253
262
  }
254
263
 
255
264
  // 3. update_memory: "actually", "correction:", "change that to"
256
265
  if (this.isUpdateMemory(lower)) {
257
- return { primary: 'update_memory', confidence: 0.85, secondary }
266
+ return ret({ primary: 'update_memory', confidence: 0.85, secondary })
258
267
  }
259
268
 
260
269
  // 4. store_this: explicit "remember:", "save this:", "I prefer" (never for questions)
261
270
  if (this.isStoreThis(lower, message)) {
262
271
  if (this.hasDecisionSignal(lower)) secondary.push('decision_made')
263
- return { primary: 'store_this', confidence: 0.90, secondary }
272
+ return ret({ primary: 'store_this', confidence: 0.90, secondary })
264
273
  }
265
274
 
266
275
  // 5. decision_made: decision phrases + reasoning (never for questions)
267
276
  if (this.isDecisionMade(lower, message)) {
268
277
  if (this.hasComparisonSignal(lower)) secondary.push('comparison')
269
- return { primary: 'decision_made', confidence: 0.85, secondary }
278
+ return ret({ primary: 'decision_made', confidence: 0.85, secondary })
270
279
  }
271
280
 
272
281
  // 6. mistake_learned: correction/bug/lesson indicators
273
282
  if (this.isMistakeLearned(lower)) {
274
- return { primary: 'mistake_learned', confidence: 0.85, secondary }
283
+ return ret({ primary: 'mistake_learned', confidence: 0.85, secondary })
275
284
  }
276
285
 
277
286
  // 7. list_all: "list all", "what decisions", "show all"
278
287
  if (this.isListAll(lower, message)) {
279
- return { primary: 'list_all', confidence: 0.85, secondary }
288
+ return ret({ primary: 'list_all', confidence: 0.85, secondary })
280
289
  }
281
290
 
282
291
  // 8. progress_update: completed task indicators (NOT questions)
283
292
  if (this.isProgressUpdate(lower, message)) {
284
293
  if (this.hasSessionSignal(lower)) secondary.push('session_start')
285
- return { primary: 'progress_update', confidence: 0.85, secondary }
294
+ return ret({ primary: 'progress_update', confidence: 0.85, secondary })
286
295
  }
287
296
 
288
297
  // 9. comparison: vs, which is better, etc.
289
298
  if (this.isComparison(lower)) {
290
299
  if (this.isQuestion(lower, message)) secondary.push('question')
291
300
  if (hasTemporal) secondary.push('exploration')
292
- return { primary: 'comparison', confidence: 0.85, secondary }
301
+ return ret({ primary: 'comparison', confidence: 0.85, secondary })
293
302
  }
294
303
 
295
304
  // 10. pattern_found: explicit pattern documentation
296
305
  if (this.isPatternFound(lower)) {
297
- return { primary: 'pattern_found', confidence: 0.80, secondary }
306
+ return ret({ primary: 'pattern_found', confidence: 0.80, secondary })
298
307
  }
299
308
 
300
309
  // 11. session_start: starting/resuming work (Phase 19: narrowed check)
301
310
  if (this.isSessionStart(lower)) {
302
311
  secondary.push('context_needed')
303
- return { primary: 'session_start', confidence: 0.90, secondary }
312
+ return ret({ primary: 'session_start', confidence: 0.90, secondary })
304
313
  }
305
314
 
306
315
  // 12. detail_request: "details obs_abc123", "show me <id>" (before exploration to avoid misclassification)
307
316
  if (this.isDetailRequest(lower)) {
308
- return { primary: 'detail_request', confidence: 0.90, secondary }
317
+ return ret({ primary: 'detail_request', confidence: 0.90, secondary })
309
318
  }
310
319
 
311
320
  // 12b. timeline: "timeline for project", "what did I do yesterday", "recent activity" (before exploration)
312
321
  if (this.isTimeline(lower)) {
313
322
  if (hasTemporal) secondary.push('exploration')
314
- return { primary: 'timeline', confidence: 0.85, secondary }
323
+ return ret({ primary: 'timeline', confidence: 0.85, secondary })
315
324
  }
316
325
 
317
326
  // 12c. exploration: trends, graph, evolution, history (general exploration that isn't a specific timeline)
318
327
  if (this.isExploration(lower)) {
319
328
  if (this.isQuestion(lower, message)) secondary.push('question')
320
329
  if (hasTemporal) secondary.push('exploration')
321
- return { primary: 'exploration', confidence: 0.75, secondary }
330
+ return ret({ primary: 'exploration', confidence: 0.75, secondary })
322
331
  }
323
332
 
324
333
  // 13. question: starts with question word or ends with ?
@@ -330,12 +339,32 @@ export class IntentClassifier {
330
339
 
331
340
  // Phase 19 B1: ? → 0.95, question word → 0.90
332
341
  const confidence = message.trim().endsWith('?') ? 0.95 : 0.90
333
- return { primary: 'question', confidence, secondary }
342
+ return ret({ primary: 'question', confidence, secondary })
334
343
  }
335
344
 
336
345
  // 14. Default: context_needed
337
346
  if (hasTemporal) secondary.push('exploration')
338
- return { primary: 'context_needed', confidence: 0.60, secondary }
347
+ const defaultResult = { primary: 'context_needed' as Intent, confidence: 0.60, secondary }
348
+ this._logTraining(message, defaultResult, startTime)
349
+ return defaultResult
350
+ }
351
+
352
+ /**
353
+ * SLM Phase 1A: Log classification result for training data collection.
354
+ * Fire-and-forget, never blocks the main path.
355
+ */
356
+ private _logTraining(message: string, result: ClassificationResult, startTime: number): void {
357
+ try {
358
+ const { logTrainingData } = require('@/training/data-store')
359
+ logTrainingData({
360
+ task: 'intent' as const,
361
+ input: message,
362
+ output: JSON.stringify({ label: result.primary, secondary: result.secondary }),
363
+ metadata: JSON.stringify({ confidence: result.confidence, elapsed_ms: Date.now() - startTime }),
364
+ })
365
+ } catch {
366
+ // Training data logging is non-critical
367
+ }
339
368
  }
340
369
 
341
370
  private isNoAction(lower: string): boolean {
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { Logger } from 'pino'
14
14
  import { IntentClassifier, type ClassificationResult } from './intent-classifier'
15
+ import type { InferenceRouter } from '@/intelligence/inference-router'
15
16
  import { BrainEntityExtractor, type BrainExtractedEntities } from './entity-extractor'
16
17
  import { ResponseFilter, type BrainResponse, type TierResults, type FilterableResult, formatCompactResponse, formatDetailResponse, formatTimeline, groupByDay } from './response-filter'
17
18
  import { SearchEngine } from './search-engine'
@@ -62,6 +63,9 @@ export class BrainRouter {
62
63
  private searchEngine: SearchEngine
63
64
  private logger: Logger
64
65
 
66
+ /** SLM Upgrade: Optional inference router for model-based classification */
67
+ private inferenceRouter: InferenceRouter | null = null
68
+
65
69
  /** Phase 30: Optional LLM compressor for long observations */
66
70
  private compressor: ObservationCompressor | null = null
67
71
 
@@ -80,6 +84,12 @@ export class BrainRouter {
80
84
  this.logger = logger.child({ component: 'brain-router' })
81
85
  }
82
86
 
87
+ /** SLM Upgrade: Set the optional inference router for model-based classification */
88
+ setInferenceRouter(router: InferenceRouter): void {
89
+ this.inferenceRouter = router
90
+ this.entityExtractor.setInferenceRouter(router)
91
+ }
92
+
83
93
  /** Phase 30: Set the optional LLM compressor */
84
94
  setCompressor(compressor: ObservationCompressor): void {
85
95
  this.compressor = compressor
@@ -120,8 +130,10 @@ export class BrainRouter {
120
130
  }
121
131
  }
122
132
 
123
- // Classify intent
124
- const classification = this.classifier.classify(message)
133
+ // Classify intent (SLM: use inference router if available, falls back to regex)
134
+ const classification = this.inferenceRouter
135
+ ? await this.inferenceRouter.classifyIntent(message)
136
+ : this.classifier.classify(message)
125
137
  this.logger.debug({ intent: classification.primary, confidence: classification.confidence }, 'Intent classified')
126
138
 
127
139
  // Route to handler
@@ -2163,6 +2175,14 @@ let routerInstance: BrainRouter | null = null
2163
2175
  export function getBrainRouter(logger: Logger): BrainRouter {
2164
2176
  if (!routerInstance) {
2165
2177
  routerInstance = new BrainRouter(logger)
2178
+ // SLM Upgrade: Wire inference router if available
2179
+ try {
2180
+ const { getInferenceRouter } = require('@/server/services')
2181
+ const ir = getInferenceRouter()
2182
+ if (ir) routerInstance.setInferenceRouter(ir)
2183
+ } catch {
2184
+ // Services not initialized yet — will use regex fallback
2185
+ }
2166
2186
  }
2167
2187
  return routerInstance
2168
2188
  }
@@ -2,8 +2,8 @@
2
2
  * List Tools Handler
3
3
  * Handles the MCP tools/list request
4
4
  *
5
- * In unified tool mode (Phase 16), only the brain() tool is exposed.
6
- * In legacy mode, all 25 tools + brain are exposed.
5
+ * In unified tool mode (default), exposes brain + search_code.
6
+ * In legacy mode, all tools are exposed.
7
7
  */
8
8
 
9
9
  import { ToolRegistry } from '@/tools/registry'
@@ -19,10 +19,10 @@ export async function handleListTools() {
19
19
  try {
20
20
  const { config } = getServices()
21
21
  if (config.unifiedToolMode) {
22
- const brainTool = ToolRegistry.getToolByName('brain')
23
- return {
24
- tools: brainTool ? [brainTool] : []
25
- }
22
+ const tools = ['brain', 'search_code']
23
+ .map(name => ToolRegistry.getToolByName(name))
24
+ .filter(Boolean)
25
+ return { tools }
26
26
  }
27
27
  } catch {
28
28
  // Services not ready, fall through to default
@@ -6,7 +6,7 @@
6
6
  import { Hono } from 'hono'
7
7
  import type { Logger } from 'pino'
8
8
  import type { Config } from '@/config'
9
- import { getMemoryService, getVaultService, isServicesInitialized } from '@/server/services'
9
+ import { getMemoryService, getVaultService, getInferenceRouter, isServicesInitialized } from '@/server/services'
10
10
  import { ResourceProvider } from '@/server/providers/resources'
11
11
  import type { MemoryManager } from '@/memory'
12
12
  import type { CapturedKnowledge, HookStats } from '@/hooks/types'
@@ -16,6 +16,7 @@ import type { CodeIndexer } from '@/code-intelligence/indexer'
16
16
  import type { CodeQuery } from '@/code-intelligence/query'
17
17
  import type { MemoryCodeLinker } from '@/code-intelligence/linker'
18
18
  import { setupWebViewer, setWebViewerCodeQuery } from '@/server/web-viewer'
19
+ import { getTrainingStats, getModelFeedbackStats, getDisagreements } from '@/training/data-store'
19
20
 
20
21
  export class HttpApiServer {
21
22
  private app: Hono
@@ -135,6 +136,12 @@ export class HttpApiServer {
135
136
 
136
137
  // Phase 23b: Expose brain://context/auto via HTTP for testability
137
138
  this.app.get('/api/context/auto', () => this.handleContextAuto())
139
+
140
+ // Phase 6A: SLM feedback & model status endpoints
141
+ this.app.get('/api/models/status', () => this.handleModelsStatus())
142
+ this.app.get('/api/models/feedback', (c) => this.handleModelsFeedback(c))
143
+ this.app.get('/api/models/disagreements', (c) => this.handleModelsDisagreements(c))
144
+ this.app.get('/api/training/stats', () => this.handleTrainingStats())
138
145
  }
139
146
 
140
147
  private async handleListProjects(): Promise<Response> {
@@ -1047,6 +1054,81 @@ export class HttpApiServer {
1047
1054
  }
1048
1055
  }
1049
1056
 
1057
+ // ─── Phase 6A: SLM Model Feedback Endpoints ────────────
1058
+
1059
+ private handleModelsStatus(): Response {
1060
+ try {
1061
+ const inferenceRouter = getInferenceRouter()
1062
+ if (!inferenceRouter) {
1063
+ return Response.json({
1064
+ success: true,
1065
+ data: { enabled: false, message: 'SLM inference not initialized' },
1066
+ })
1067
+ }
1068
+ return Response.json({ success: true, data: inferenceRouter.getStatus() })
1069
+ } catch (error) {
1070
+ this.logger.error({ error }, 'Failed to get model status')
1071
+ return Response.json(
1072
+ { success: false, error: 'Failed to get model status' },
1073
+ { status: 500 }
1074
+ )
1075
+ }
1076
+ }
1077
+
1078
+ private handleModelsFeedback(c: any): Response {
1079
+ try {
1080
+ const task = c.req.query('task')
1081
+ const stats = getModelFeedbackStats()
1082
+
1083
+ if (task) {
1084
+ const taskStats = stats[task]
1085
+ if (!taskStats) {
1086
+ return Response.json(
1087
+ { success: false, error: `Unknown task: ${task}` },
1088
+ { status: 400 }
1089
+ )
1090
+ }
1091
+ return Response.json({ success: true, data: { [task]: taskStats } })
1092
+ }
1093
+
1094
+ return Response.json({ success: true, data: stats })
1095
+ } catch (error) {
1096
+ this.logger.error({ error }, 'Failed to get model feedback stats')
1097
+ return Response.json(
1098
+ { success: false, error: 'Failed to get model feedback stats' },
1099
+ { status: 500 }
1100
+ )
1101
+ }
1102
+ }
1103
+
1104
+ private handleModelsDisagreements(c: any): Response {
1105
+ try {
1106
+ const task = c.req.query('task') || 'intent'
1107
+ const limit = parseInt(c.req.query('limit') || '50', 10)
1108
+ const disagreements = getDisagreements(task, limit)
1109
+ return Response.json({ success: true, data: disagreements })
1110
+ } catch (error) {
1111
+ this.logger.error({ error }, 'Failed to get model disagreements')
1112
+ return Response.json(
1113
+ { success: false, error: 'Failed to get model disagreements' },
1114
+ { status: 500 }
1115
+ )
1116
+ }
1117
+ }
1118
+
1119
+ private handleTrainingStats(): Response {
1120
+ try {
1121
+ const stats = getTrainingStats()
1122
+ return Response.json({ success: true, data: stats })
1123
+ } catch (error) {
1124
+ this.logger.error({ error }, 'Failed to get training stats')
1125
+ return Response.json(
1126
+ { success: false, error: 'Failed to get training stats' },
1127
+ { status: 500 }
1128
+ )
1129
+ }
1130
+ }
1131
+
1050
1132
  async start(): Promise<void> {
1051
1133
  const port = this.config.port || 3000
1052
1134
 
@@ -26,6 +26,8 @@ import { SemanticCache } from '@/intelligence/optimization/semantic-cache'
26
26
  import { PrecomputeEngine } from '@/intelligence/optimization/precompute'
27
27
  import { MemoryArchiver } from '@/memory/consolidation/archiver'
28
28
  import { ImportanceScorer } from '@/memory/consolidation/scorer'
29
+ import { ModelManager } from '@/intelligence/model-manager'
30
+ import { InferenceRouter } from '@/intelligence/inference-router'
29
31
 
30
32
  export interface KnowledgeGraphServiceContainer {
31
33
  graph: InMemoryKnowledgeGraph
@@ -53,6 +55,8 @@ export interface Services {
53
55
  codeIndexer: CodeIndexer | null
54
56
  codeQuery: CodeQuery | null
55
57
  codeLinker: MemoryCodeLinker | null
58
+ modelManager: ModelManager | null
59
+ inferenceRouter: InferenceRouter | null
56
60
  logger: Logger
57
61
  config: Config
58
62
  }
@@ -371,6 +375,25 @@ export async function initializeServices(config: Config, logger: Logger): Promis
371
375
  serviceLogger.warn({ error }, 'Failed to initialize code linker, continuing without it')
372
376
  }
373
377
 
378
+ // Initialize SLM Model Manager & Inference Router
379
+ let modelManager: ModelManager | null = null
380
+ let inferenceRouter: InferenceRouter | null = null
381
+ try {
382
+ const slmModelsDir = config.slm?.modelsDir?.replace(/^~/, require('os').homedir())
383
+ modelManager = new ModelManager(serviceLogger, slmModelsDir)
384
+ inferenceRouter = new InferenceRouter(serviceLogger, config, modelManager)
385
+ const slmEnabled = config.slm?.enabled ?? false
386
+ serviceLogger.info({ enabled: slmEnabled }, 'SLM inference router initialized')
387
+ } catch (error) {
388
+ serviceLogger.warn({ error }, 'Failed to initialize SLM inference, continuing with regex only')
389
+ }
390
+
391
+ // Wire SLM inference into PatternRecognizer (Phase 4C)
392
+ if (inferenceRouter && phase12) {
393
+ phase12.patterns.setInferenceRouter(inferenceRouter)
394
+ serviceLogger.info('SLM inference wired into PatternRecognizer')
395
+ }
396
+
374
397
  // Store services
375
398
  services = {
376
399
  memory,
@@ -388,6 +411,8 @@ export async function initializeServices(config: Config, logger: Logger): Promis
388
411
  codeIndexer,
389
412
  codeQuery,
390
413
  codeLinker,
414
+ modelManager,
415
+ inferenceRouter,
391
416
  logger,
392
417
  config
393
418
  }
@@ -526,6 +551,22 @@ export function getCodeLinker(): MemoryCodeLinker | null {
526
551
  return services?.codeLinker ?? null
527
552
  }
528
553
 
554
+ /**
555
+ * Get Model Manager (SLM Upgrade)
556
+ * Returns null if SLM is not initialized
557
+ */
558
+ export function getModelManager(): ModelManager | null {
559
+ return services?.modelManager ?? null
560
+ }
561
+
562
+ /**
563
+ * Get Inference Router (SLM Upgrade)
564
+ * Returns null if SLM is not initialized
565
+ */
566
+ export function getInferenceRouter(): InferenceRouter | null {
567
+ return services?.inferenceRouter ?? null
568
+ }
569
+
529
570
  /**
530
571
  * Check if services are initialized
531
572
  */
@@ -617,6 +658,12 @@ export async function shutdownServices(): Promise<void> {
617
658
  }
618
659
  }
619
660
 
661
+ // Unload SLM models
662
+ if (services.modelManager) {
663
+ services.modelManager.unloadAll()
664
+ serviceLogger.info('SLM models unloaded')
665
+ }
666
+
620
667
  // Cleanup Phase 12
621
668
  services.phase12.cleanup()
622
669