claude-brain 0.9.2 → 0.10.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.
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Search Engine
3
+ * Phase 19: Wires all advanced intelligence features into a unified search path.
4
+ *
5
+ * This is the core intelligence layer between the router and raw memory.
6
+ * Provides: hybrid search, semantic caching, temporal filtering, timeouts.
7
+ */
8
+
9
+ import type { Logger } from 'pino'
10
+ import type { NormalizedResult } from './types'
11
+ import { normalizeSearchResults, normalizePatternResults, normalizeCorrectionResults } from './types'
12
+ import {
13
+ getMemoryService,
14
+ getSemanticCacheService,
15
+ getRetrievalPipeline,
16
+ getKnowledgeGraphService,
17
+ getEpisodeService,
18
+ isServicesInitialized
19
+ } from '@/server/services'
20
+
21
+ /**
22
+ * Wrap a promise with a timeout. Returns fallback if the promise doesn't resolve in time.
23
+ */
24
+ function withTimeout<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T> {
25
+ return Promise.race([
26
+ promise,
27
+ new Promise<T>(resolve => setTimeout(() => resolve(fallback), ms))
28
+ ])
29
+ }
30
+
31
+ const SEARCH_TIMEOUT = 5000 // 5s timeout for all searches
32
+
33
+ export class SearchEngine {
34
+ private logger: Logger
35
+
36
+ constructor(logger: Logger) {
37
+ this.logger = logger.child({ component: 'search-engine' })
38
+ }
39
+
40
+ /**
41
+ * Enhanced search — uses hybrid retrieval pipeline when available,
42
+ * falls back to plain searchRaw().
43
+ * Wraps with semantic cache for repeated queries.
44
+ */
45
+ async enhancedSearch(
46
+ query: string,
47
+ options?: { project?: string; limit?: number; minSimilarity?: number }
48
+ ): Promise<NormalizedResult[]> {
49
+ if (!isServicesInitialized()) return []
50
+
51
+ const cacheKey = `search:${query}:${options?.project || ''}:${options?.limit || 5}`
52
+
53
+ // Check semantic cache first
54
+ const cache = getSemanticCacheService()
55
+ if (cache) {
56
+ try {
57
+ const cached = await cache.get(cacheKey)
58
+ if (cached) {
59
+ this.logger.debug({ query }, 'Search cache hit')
60
+ return cached as NormalizedResult[]
61
+ }
62
+ } catch {
63
+ // Cache miss or error, continue
64
+ }
65
+ }
66
+
67
+ // Try hybrid search via RetrievalPipeline
68
+ const pipeline = getRetrievalPipeline()
69
+ if (pipeline) {
70
+ try {
71
+ const hybridResults = await withTimeout(
72
+ pipeline.search(query, {
73
+ project: options?.project,
74
+ limit: options?.limit || 5,
75
+ minSimilarity: options?.minSimilarity || 0.3
76
+ }),
77
+ SEARCH_TIMEOUT,
78
+ []
79
+ )
80
+
81
+ if (hybridResults.length > 0) {
82
+ const normalized = hybridResults.map((r: any) => ({
83
+ id: r.id || '',
84
+ content: r.content || r.document || '',
85
+ score: r.score || r.similarity || 0,
86
+ source: 'decision' as const,
87
+ project: r.metadata?.project || options?.project || '',
88
+ date: r.metadata?.created_at || '',
89
+ metadata: r.metadata || {}
90
+ }))
91
+
92
+ // Cache results
93
+ if (cache) {
94
+ try { await cache.set(cacheKey, normalized) } catch {}
95
+ }
96
+ return normalized
97
+ }
98
+ } catch (error) {
99
+ this.logger.debug({ error }, 'Hybrid search failed, falling back to plain search')
100
+ }
101
+ }
102
+
103
+ // Fallback: plain searchRaw
104
+ return this.plainSearch(query, options, cache, cacheKey)
105
+ }
106
+
107
+ /**
108
+ * Plain search using memory.searchRaw() — always available.
109
+ */
110
+ async plainSearch(
111
+ query: string,
112
+ options?: { project?: string; limit?: number; minSimilarity?: number },
113
+ cache?: any,
114
+ cacheKey?: string
115
+ ): Promise<NormalizedResult[]> {
116
+ if (!isServicesInitialized()) return []
117
+
118
+ const memory = getMemoryService()
119
+ try {
120
+ const rawResults = await withTimeout(
121
+ memory.searchRaw(query, {
122
+ project: options?.project,
123
+ limit: options?.limit || 5,
124
+ minSimilarity: options?.minSimilarity || 0.3
125
+ }),
126
+ SEARCH_TIMEOUT,
127
+ []
128
+ )
129
+ const normalized = normalizeSearchResults(rawResults)
130
+
131
+ // Cache results
132
+ if (cache && cacheKey) {
133
+ try { await cache.set(cacheKey, normalized) } catch {}
134
+ }
135
+
136
+ return normalized
137
+ } catch {
138
+ return []
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Search patterns with normalization and timeout
144
+ */
145
+ async searchPatterns(
146
+ query: string,
147
+ options?: { project?: string; limit?: number; minSimilarity?: number }
148
+ ): Promise<NormalizedResult[]> {
149
+ if (!isServicesInitialized()) return []
150
+ const memory = getMemoryService()
151
+ try {
152
+ const results = await withTimeout(
153
+ memory.searchPatterns(query, {
154
+ project: options?.project,
155
+ limit: options?.limit || 3,
156
+ minSimilarity: options?.minSimilarity || 0.3
157
+ }),
158
+ SEARCH_TIMEOUT,
159
+ []
160
+ )
161
+ return normalizePatternResults(results)
162
+ } catch {
163
+ return []
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Search corrections with normalization and timeout
169
+ */
170
+ async searchCorrections(
171
+ query: string,
172
+ options?: { project?: string; limit?: number; minSimilarity?: number }
173
+ ): Promise<NormalizedResult[]> {
174
+ if (!isServicesInitialized()) return []
175
+ const memory = getMemoryService()
176
+ try {
177
+ const results = await withTimeout(
178
+ memory.searchCorrections(query, {
179
+ project: options?.project,
180
+ limit: options?.limit || 3,
181
+ minSimilarity: options?.minSimilarity || 0.3
182
+ }),
183
+ SEARCH_TIMEOUT,
184
+ []
185
+ )
186
+ return normalizeCorrectionResults(results)
187
+ } catch {
188
+ return []
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Search knowledge graph for related concepts
194
+ */
195
+ async searchGraph(query: string, limit: number = 5): Promise<NormalizedResult[]> {
196
+ try {
197
+ const kgService = getKnowledgeGraphService()
198
+ if (!kgService?.search) return []
199
+
200
+ const graphResults = kgService.search.search({ query, limit })
201
+ if (!graphResults?.nodes?.length) return []
202
+
203
+ return graphResults.nodes.map((n: any) => ({
204
+ id: n.id || '',
205
+ content: `**${n.label || n.name}** (${n.type})${n.metadata?.description ? `\n${n.metadata.description}` : ''}`,
206
+ score: n.score || 0.5,
207
+ source: 'graph' as const,
208
+ project: n.metadata?.project || '',
209
+ date: '',
210
+ metadata: n.metadata || {}
211
+ }))
212
+ } catch {
213
+ return []
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Temporal search — parse temporal phrases from query, filter results by date range.
219
+ * Uses TemporalQueryProcessor when available.
220
+ */
221
+ async temporalSearch(
222
+ query: string,
223
+ options?: { project?: string; limit?: number }
224
+ ): Promise<{ results: NormalizedResult[]; cleanedQuery: string }> {
225
+ if (!isServicesInitialized()) return { results: [], cleanedQuery: query }
226
+
227
+ try {
228
+ // Dynamic import to avoid hard dependency
229
+ const { TemporalQueryProcessor } = await import('@/temporal/query-processor')
230
+ const processor = new TemporalQueryProcessor(this.logger)
231
+ const parsed = processor.parse(query)
232
+
233
+ if (parsed.intent === 'none') {
234
+ // No temporal expressions found, use normal search
235
+ const results = await this.enhancedSearch(query, options)
236
+ return { results, cleanedQuery: query }
237
+ }
238
+
239
+ // Search with cleaned query (temporal phrases removed)
240
+ const results = await this.enhancedSearch(parsed.cleaned || query, {
241
+ ...options,
242
+ limit: (options?.limit || 5) * 2 // Fetch more since we'll filter by date
243
+ })
244
+
245
+ // Filter results by date range
246
+ const filtered = results.filter(r => {
247
+ if (!r.date) return true // Keep results without dates
248
+ const date = new Date(r.date)
249
+ if (isNaN(date.getTime())) return true
250
+ if (parsed.startDate && date < parsed.startDate) return false
251
+ if (parsed.endDate && date > parsed.endDate) return false
252
+ return true
253
+ })
254
+
255
+ return {
256
+ results: filtered.slice(0, options?.limit || 5),
257
+ cleanedQuery: parsed.cleaned || query
258
+ }
259
+ } catch {
260
+ // TemporalQueryProcessor not available, fall back
261
+ const results = await this.enhancedSearch(query, options)
262
+ return { results, cleanedQuery: query }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Build a timeline for a topic/project
268
+ */
269
+ async buildTimeline(
270
+ options?: { project?: string; topic?: string; limit?: number }
271
+ ): Promise<any | null> {
272
+ if (!isServicesInitialized()) return null
273
+
274
+ try {
275
+ const memory = getMemoryService()
276
+ if (!memory.isChromaDBEnabled()) return null
277
+
278
+ const { TimelineBuilder } = await import('@/temporal/timeline')
279
+ const builder = new TimelineBuilder(
280
+ this.logger,
281
+ memory.chroma.collections,
282
+ memory.chroma.embeddings
283
+ )
284
+ return await withTimeout(
285
+ builder.buildTimeline(options),
286
+ SEARCH_TIMEOUT,
287
+ null
288
+ )
289
+ } catch {
290
+ return null
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Analyze decision evolution for a topic
296
+ */
297
+ async analyzeEvolution(
298
+ topic: string,
299
+ options?: { project?: string; limit?: number }
300
+ ): Promise<any | null> {
301
+ if (!isServicesInitialized()) return null
302
+
303
+ try {
304
+ const memory = getMemoryService()
305
+ if (!memory.isChromaDBEnabled()) return null
306
+
307
+ const { DecisionEvolutionTracker } = await import('@/temporal/evolution')
308
+ const tracker = new DecisionEvolutionTracker(
309
+ this.logger,
310
+ memory.chroma.collections,
311
+ memory.chroma.embeddings
312
+ )
313
+ return await withTimeout(
314
+ tracker.analyzeEvolution(topic, options),
315
+ SEARCH_TIMEOUT,
316
+ null
317
+ )
318
+ } catch {
319
+ return null
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Detect trends
325
+ */
326
+ async detectTrends(
327
+ options?: { project?: string; periodDays?: number; limit?: number }
328
+ ): Promise<any | null> {
329
+ if (!isServicesInitialized()) return null
330
+
331
+ try {
332
+ const memory = getMemoryService()
333
+ if (!memory.isChromaDBEnabled()) return null
334
+
335
+ const { TrendDetector } = await import('@/temporal/trends')
336
+ const detector = new TrendDetector(this.logger, memory.chroma.collections)
337
+ return await withTimeout(
338
+ detector.detectTrends(options),
339
+ SEARCH_TIMEOUT,
340
+ null
341
+ )
342
+ } catch {
343
+ return null
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Multi-hop chain retrieval for complex questions
349
+ */
350
+ async chainSearch(
351
+ query: string,
352
+ options?: { project?: string; maxHops?: number }
353
+ ): Promise<any | null> {
354
+ if (!isServicesInitialized()) return null
355
+
356
+ try {
357
+ const memory = getMemoryService()
358
+ if (!memory.isChromaDBEnabled()) return null
359
+
360
+ const { ChainRetrieval } = await import('@/reasoning/chain-retrieval')
361
+ const chain = new ChainRetrieval(
362
+ this.logger,
363
+ memory.chroma.collections,
364
+ memory.chroma.embeddings
365
+ )
366
+ return await withTimeout(
367
+ chain.retrieve(query, {
368
+ maxHops: options?.maxHops || 3,
369
+ project: options?.project
370
+ }),
371
+ SEARCH_TIMEOUT,
372
+ null
373
+ )
374
+ } catch {
375
+ return null
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Get recommendations based on context
381
+ */
382
+ async getRecommendations(
383
+ query: string,
384
+ options?: { project?: string; limit?: number }
385
+ ): Promise<any | null> {
386
+ if (!isServicesInitialized()) return null
387
+
388
+ try {
389
+ const memory = getMemoryService()
390
+ if (!memory.isChromaDBEnabled()) return null
391
+
392
+ const { Recommender } = await import('@/prediction/recommender')
393
+ const recommender = new Recommender(
394
+ this.logger,
395
+ memory.chroma.collections,
396
+ memory.chroma.embeddings
397
+ )
398
+ return await withTimeout(
399
+ recommender.getRecommendations(query, {
400
+ project: options?.project,
401
+ limit: options?.limit || 5
402
+ }),
403
+ SEARCH_TIMEOUT,
404
+ null
405
+ )
406
+ } catch {
407
+ return null
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Find cross-project patterns
413
+ */
414
+ async findCrossProjectPatterns(
415
+ options?: { query?: string; minProjects?: number; limit?: number }
416
+ ): Promise<any | null> {
417
+ if (!isServicesInitialized()) return null
418
+
419
+ try {
420
+ const memory = getMemoryService()
421
+ if (!memory.isChromaDBEnabled()) return null
422
+
423
+ const { PatternGeneralizer } = await import('@/cross-project/generalizer')
424
+ const generalizer = new PatternGeneralizer(
425
+ this.logger,
426
+ memory.chroma.collections,
427
+ memory.chroma.embeddings
428
+ )
429
+ return await withTimeout(
430
+ generalizer.findCrossProjectPatterns(options),
431
+ SEARCH_TIMEOUT,
432
+ null
433
+ )
434
+ } catch {
435
+ return null
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Invalidate cache entries for a project after store/update/delete
441
+ */
442
+ invalidateCache(project?: string): void {
443
+ try {
444
+ const cache = getSemanticCacheService()
445
+ if (!cache) return
446
+ if (project) {
447
+ cache.invalidateProject(project)
448
+ } else {
449
+ cache.clear()
450
+ }
451
+ } catch {
452
+ // Cache not available
453
+ }
454
+ }
455
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Routing Types
3
+ * Phase 19: Normalized result types for the brain() router
4
+ *
5
+ * Eliminates scattered `r.decision?.decision || r.content?.slice(0, 300)`
6
+ * by normalizing all search results to a flat structure.
7
+ */
8
+
9
+ export interface NormalizedResult {
10
+ id: string
11
+ content: string
12
+ score: number
13
+ source: 'decision' | 'pattern' | 'correction' | 'memory' | 'graph' | 'episode' | 'cross-project'
14
+ project?: string
15
+ date?: string
16
+ metadata?: Record<string, unknown>
17
+ }
18
+
19
+ /**
20
+ * Normalize raw searchRaw() results into flat NormalizedResult[]
21
+ * Handles the nested { memory: { id }, decision: { id } } structure
22
+ */
23
+ export function normalizeSearchResults(rawResults: any[]): NormalizedResult[] {
24
+ return rawResults.map(r => {
25
+ const id = r.memory?.id || r.decision?.id || r.id || ''
26
+ const project = r.memory?.project || r.decision?.project || r.metadata?.project || ''
27
+ const date = r.memory?.createdAt || r.decision?.createdAt || r.metadata?.created_at || ''
28
+
29
+ // Prefer decision content, fall back to memory content
30
+ let content: string
31
+ if (r.decision?.decision) {
32
+ content = r.decision.decision
33
+ if (r.decision.reasoning) {
34
+ content = `**${content}**\n${r.decision.reasoning}`
35
+ }
36
+ } else if (r.memory?.content) {
37
+ content = r.memory.content
38
+ } else if (r.content) {
39
+ content = typeof r.content === 'string' ? r.content : JSON.stringify(r.content)
40
+ } else {
41
+ content = ''
42
+ }
43
+
44
+ return {
45
+ id,
46
+ content,
47
+ score: r.similarity || r.score || 0,
48
+ source: r.decision ? 'decision' as const : 'memory' as const,
49
+ project: typeof project === 'string' ? project : String(project),
50
+ date: date instanceof Date ? date.toISOString() : typeof date === 'string' ? date : '',
51
+ metadata: r.metadata || r.decision || r.memory?.metadata || {}
52
+ }
53
+ })
54
+ }
55
+
56
+ /**
57
+ * Normalize pattern search results
58
+ */
59
+ export function normalizePatternResults(results: any[]): NormalizedResult[] {
60
+ return results.map(p => ({
61
+ id: p.id || '',
62
+ content: p.metadata?.description || p.description || p.content || '',
63
+ score: p.similarity || p.score || 0,
64
+ source: 'pattern' as const,
65
+ project: p.metadata?.project || p.project || '',
66
+ date: p.metadata?.created_at || '',
67
+ metadata: p.metadata || {}
68
+ }))
69
+ }
70
+
71
+ /**
72
+ * Normalize correction search results
73
+ */
74
+ export function normalizeCorrectionResults(results: any[]): NormalizedResult[] {
75
+ return results.map(c => ({
76
+ id: c.id || '',
77
+ content: `Original: ${c.metadata?.original || c.original || ''}\nFix: ${c.metadata?.correction || c.correction || c.content || ''}`,
78
+ score: c.similarity || c.score || 0,
79
+ source: 'correction' as const,
80
+ project: c.metadata?.project || c.project || '',
81
+ date: c.metadata?.created_at || '',
82
+ metadata: c.metadata || {}
83
+ }))
84
+ }
@@ -31,23 +31,11 @@ import { handleGetCorrections } from './tools/get-corrections'
31
31
  import { handleCreateProject } from './tools/create-project'
32
32
  import { handleInitProject } from './tools/init-project'
33
33
 
34
- // Phase 13 Retrieval handlers
35
- import { handleRateMemory } from './tools/rate-memory'
36
-
37
- // Phase 14 Knowledge Graph & Episodic Memory handlers
38
- import { handleSearchKnowledgeGraph } from './tools/search-knowledge-graph'
39
- import { handleGetEpisode } from './tools/get-episode'
40
- import { handleListEpisodes } from './tools/list-episodes'
41
-
42
- // Phase 15 Advanced Intelligence & Temporal Reasoning handlers
43
- import { handleGetDecisionTimeline } from './tools/get-decision-timeline'
44
- import { handleAnalyzeDecisionEvolution } from './tools/analyze-decision-evolution'
45
- import { handleDetectTrends } from './tools/detect-trends'
46
- import { handleWhatIfAnalysis } from './tools/what-if-analysis'
47
- import { handleGetRecommendations } from './tools/get-recommendations'
48
- import { handleFindCrossProjectPatterns } from './tools/find-cross-project-patterns'
49
-
50
34
  // Phase 16 Unified Brain Tool
35
+ // Phase 19: Removed 9 redundant tool handlers (rate_memory, search_knowledge_graph,
36
+ // get_episode, list_episodes, get_decision_timeline, analyze_decision_evolution,
37
+ // detect_trends, what_if_analysis, get_recommendations, find_cross_project_patterns).
38
+ // Their functionality is now absorbed into brain().
51
39
  import { handleBrain } from './tools/brain'
52
40
 
53
41
  /**
@@ -134,39 +122,6 @@ export async function handleCallTool(
134
122
  case 'get_corrections':
135
123
  return await handleGetCorrections(args, logger)
136
124
 
137
- // Phase 13 Retrieval Tools
138
- case 'rate_memory':
139
- return await handleRateMemory(args, logger)
140
-
141
- // Phase 14 Knowledge Graph & Episodic Memory Tools
142
- case 'search_knowledge_graph':
143
- return await handleSearchKnowledgeGraph(args, logger)
144
-
145
- case 'get_episode':
146
- return await handleGetEpisode(args, logger)
147
-
148
- case 'list_episodes':
149
- return await handleListEpisodes(args, logger)
150
-
151
- // Phase 15 Advanced Intelligence & Temporal Reasoning Tools
152
- case 'get_decision_timeline':
153
- return await handleGetDecisionTimeline(args, logger)
154
-
155
- case 'analyze_decision_evolution':
156
- return await handleAnalyzeDecisionEvolution(args, logger)
157
-
158
- case 'detect_trends':
159
- return await handleDetectTrends(args, logger)
160
-
161
- case 'what_if_analysis':
162
- return await handleWhatIfAnalysis(args, logger)
163
-
164
- case 'get_recommendations':
165
- return await handleGetRecommendations(args, logger)
166
-
167
- case 'find_cross_project_patterns':
168
- return await handleFindCrossProjectPatterns(args, logger)
169
-
170
125
  // Phase 16 Unified Brain Tool
171
126
  case 'brain':
172
127
  return await handleBrain(args, logger)
@@ -24,10 +24,8 @@ export { handleGetCorrections } from './get-corrections'
24
24
  export { handleCreateProject } from './create-project'
25
25
  export { handleInitProject } from './init-project'
26
26
 
27
- // Phase 15 - Advanced Intelligence & Temporal Reasoning
28
- export { handleGetDecisionTimeline } from './get-decision-timeline'
29
- export { handleAnalyzeDecisionEvolution } from './analyze-decision-evolution'
30
- export { handleDetectTrends } from './detect-trends'
31
- export { handleWhatIfAnalysis } from './what-if-analysis'
32
- export { handleGetRecommendations } from './get-recommendations'
33
- export { handleFindCrossProjectPatterns } from './find-cross-project-patterns'
27
+ // Phase 19: Removed 9 redundant tool exports.
28
+ // rate_memory, search_knowledge_graph, get_episode, list_episodes,
29
+ // get_decision_timeline, analyze_decision_evolution, detect_trends,
30
+ // what_if_analysis, get_recommendations, find_cross_project_patterns
31
+ // are now absorbed into brain(). Handler files kept for reference.
@@ -11,6 +11,7 @@ import { VaultManager } from '@/vault'
11
11
  import { ContextManager } from '@/context'
12
12
  import { Phase12Manager } from '@/phase12'
13
13
  import { RetrievalService } from '@/retrieval/service'
14
+ import { RetrievalPipeline } from '@/retrieval/pipeline'
14
15
  import { InMemoryKnowledgeGraph } from '@/knowledge/graph/memory-graph'
15
16
  import { GraphSearchEngine } from '@/knowledge/graph/search'
16
17
  import { KnowledgeGraphBuilder } from '@/knowledge/graph/builder'
@@ -36,6 +37,7 @@ export interface Services {
36
37
  context: ContextManager
37
38
  phase12: Phase12Manager
38
39
  retrieval: RetrievalService | null
40
+ retrievalPipeline: RetrievalPipeline | null
39
41
  knowledgeGraph: KnowledgeGraphServiceContainer | null
40
42
  episodeManager: EpisodeManager | null
41
43
  semanticCache: SemanticCache | null
@@ -127,6 +129,23 @@ export async function initializeServices(config: Config, logger: Logger): Promis
127
129
  serviceLogger.warn('Retrieval service requires ChromaDB, skipping initialization')
128
130
  }
129
131
 
132
+ // Initialize Retrieval Pipeline (Phase 19) — hybrid search with BM25 + semantic + fusion
133
+ let retrievalPipeline: RetrievalPipeline | null = null
134
+ if (config.retrieval?.enabled && memory.isChromaDBEnabled()) {
135
+ try {
136
+ retrievalPipeline = new RetrievalPipeline(
137
+ logger,
138
+ memory.chroma.collections,
139
+ memory.chroma.embeddings,
140
+ config.retrieval
141
+ )
142
+ await retrievalPipeline.initialize()
143
+ serviceLogger.info('Retrieval pipeline initialized (hybrid search)')
144
+ } catch (error) {
145
+ serviceLogger.warn({ error }, 'Failed to initialize retrieval pipeline, continuing without hybrid search')
146
+ }
147
+ }
148
+
130
149
  // Initialize Knowledge Graph Service (Phase 14)
131
150
  let knowledgeGraph: KnowledgeGraphServiceContainer | null = null
132
151
  if (config.knowledge?.graph?.enabled !== false) {
@@ -221,6 +240,7 @@ export async function initializeServices(config: Config, logger: Logger): Promis
221
240
  context,
222
241
  phase12,
223
242
  retrieval,
243
+ retrievalPipeline,
224
244
  knowledgeGraph,
225
245
  episodeManager,
226
246
  semanticCache,
@@ -283,6 +303,14 @@ export function getRetrievalService(): RetrievalService | null {
283
303
  return getServices().retrieval
284
304
  }
285
305
 
306
+ /**
307
+ * Get Retrieval Pipeline (Phase 19)
308
+ * Returns null if hybrid search is not enabled
309
+ */
310
+ export function getRetrievalPipeline(): RetrievalPipeline | null {
311
+ return getServices().retrievalPipeline
312
+ }
313
+
286
314
  /**
287
315
  * Get Knowledge Graph service
288
316
  * Returns null if knowledge graph is not enabled