memento-mcp-server 1.13.1-a4 → 1.14.0-C

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.
Files changed (118) hide show
  1. package/dist/database/schema.sql +9 -2
  2. package/dist/domains/memory/tools/recall-tool.d.ts +190 -0
  3. package/dist/domains/memory/tools/recall-tool.d.ts.map +1 -1
  4. package/dist/domains/memory/tools/recall-tool.js +488 -14
  5. package/dist/domains/memory/tools/recall-tool.js.map +1 -1
  6. package/dist/domains/memory/tools/remember-tool.d.ts +10 -0
  7. package/dist/domains/memory/tools/remember-tool.d.ts.map +1 -1
  8. package/dist/domains/memory/tools/remember-tool.js +264 -35
  9. package/dist/domains/memory/tools/remember-tool.js.map +1 -1
  10. package/dist/domains/relation/services/relation-quality-validator.d.ts.map +1 -1
  11. package/dist/domains/relation/services/relation-quality-validator.js +15 -10
  12. package/dist/domains/relation/services/relation-quality-validator.js.map +1 -1
  13. package/dist/domains/relation/services/rule-based-relation-extractor.d.ts.map +1 -1
  14. package/dist/domains/relation/services/rule-based-relation-extractor.js +18 -0
  15. package/dist/domains/relation/services/rule-based-relation-extractor.js.map +1 -1
  16. package/dist/domains/relation/tools/add-relation-tool.d.ts +3 -3
  17. package/dist/domains/relation/tools/add-relation-tool.js +2 -2
  18. package/dist/domains/relation/tools/add-relation-tool.js.map +1 -1
  19. package/dist/domains/relation/tools/get-relations-tool.d.ts +3 -3
  20. package/dist/domains/relation/tools/get-relations-tool.js +2 -2
  21. package/dist/domains/relation/tools/get-relations-tool.js.map +1 -1
  22. package/dist/domains/search/algorithms/hybrid-search-engine.d.ts +20 -0
  23. package/dist/domains/search/algorithms/hybrid-search-engine.d.ts.map +1 -1
  24. package/dist/domains/search/algorithms/hybrid-search-engine.js +168 -5
  25. package/dist/domains/search/algorithms/hybrid-search-engine.js.map +1 -1
  26. package/dist/domains/search/algorithms/search-engine.d.ts.map +1 -1
  27. package/dist/domains/search/algorithms/search-engine.js +37 -17
  28. package/dist/domains/search/algorithms/search-engine.js.map +1 -1
  29. package/dist/domains/search/algorithms/search-ranking.d.ts +15 -2
  30. package/dist/domains/search/algorithms/search-ranking.d.ts.map +1 -1
  31. package/dist/domains/search/algorithms/search-ranking.js +46 -15
  32. package/dist/domains/search/algorithms/search-ranking.js.map +1 -1
  33. package/dist/domains/search/repositories/vector-search.repository.d.ts.map +1 -1
  34. package/dist/domains/search/repositories/vector-search.repository.js +180 -89
  35. package/dist/domains/search/repositories/vector-search.repository.js.map +1 -1
  36. package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.d.ts +63 -0
  37. package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.d.ts.map +1 -0
  38. package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.js +257 -0
  39. package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.js.map +1 -0
  40. package/dist/infrastructure/database/database/migration/migrations/007-procedural-memory-enhancement.sql +66 -0
  41. package/dist/infrastructure/reflexion-worker.d.ts +18 -0
  42. package/dist/infrastructure/reflexion-worker.d.ts.map +1 -1
  43. package/dist/infrastructure/reflexion-worker.js +216 -0
  44. package/dist/infrastructure/reflexion-worker.js.map +1 -1
  45. package/dist/infrastructure/scheduler/batch-scheduler.d.ts +51 -8
  46. package/dist/infrastructure/scheduler/batch-scheduler.d.ts.map +1 -1
  47. package/dist/infrastructure/scheduler/batch-scheduler.js +299 -205
  48. package/dist/infrastructure/scheduler/batch-scheduler.js.map +1 -1
  49. package/dist/infrastructure/scheduler/file-logger.d.ts +82 -0
  50. package/dist/infrastructure/scheduler/file-logger.d.ts.map +1 -0
  51. package/dist/infrastructure/scheduler/file-logger.js +133 -0
  52. package/dist/infrastructure/scheduler/file-logger.js.map +1 -0
  53. package/dist/infrastructure/scheduler/health-checker.d.ts +54 -0
  54. package/dist/infrastructure/scheduler/health-checker.d.ts.map +1 -0
  55. package/dist/infrastructure/scheduler/health-checker.js +96 -0
  56. package/dist/infrastructure/scheduler/health-checker.js.map +1 -0
  57. package/dist/infrastructure/scheduler/job-queue.d.ts +85 -0
  58. package/dist/infrastructure/scheduler/job-queue.d.ts.map +1 -0
  59. package/dist/infrastructure/scheduler/job-queue.js +125 -0
  60. package/dist/infrastructure/scheduler/job-queue.js.map +1 -0
  61. package/dist/infrastructure/scheduler/relation-validator-executor.d.ts +37 -0
  62. package/dist/infrastructure/scheduler/relation-validator-executor.d.ts.map +1 -0
  63. package/dist/infrastructure/scheduler/relation-validator-executor.js +120 -0
  64. package/dist/infrastructure/scheduler/relation-validator-executor.js.map +1 -0
  65. package/dist/infrastructure/scheduler/retry-manager.d.ts +62 -0
  66. package/dist/infrastructure/scheduler/retry-manager.d.ts.map +1 -0
  67. package/dist/infrastructure/scheduler/retry-manager.js +91 -0
  68. package/dist/infrastructure/scheduler/retry-manager.js.map +1 -0
  69. package/dist/npm-client/utils.d.ts.map +1 -1
  70. package/dist/npm-client/utils.js +2 -1
  71. package/dist/npm-client/utils.js.map +1 -1
  72. package/dist/scripts/copy-assets.js +4 -4
  73. package/dist/scripts/copy-assets.js.map +1 -1
  74. package/dist/server/http-server.d.ts.map +1 -1
  75. package/dist/server/http-server.js +15 -17
  76. package/dist/server/http-server.js.map +1 -1
  77. package/dist/services/anchor-manager.d.ts.map +1 -1
  78. package/dist/services/anchor-manager.js.map +1 -1
  79. package/dist/shared/types/index.d.ts +36 -0
  80. package/dist/shared/types/index.d.ts.map +1 -1
  81. package/dist/shared/types/index.js.map +1 -1
  82. package/dist/shared/types/relation.d.ts +1 -1
  83. package/dist/shared/types/relation.d.ts.map +1 -1
  84. package/dist/shared/types/relation.js +7 -4
  85. package/dist/shared/types/relation.js.map +1 -1
  86. package/dist/shared/utils/database.d.ts.map +1 -1
  87. package/dist/shared/utils/database.js +9 -2
  88. package/dist/shared/utils/database.js.map +1 -1
  89. package/dist/shared/utils/procedural-memory-extractor.d.ts +108 -0
  90. package/dist/shared/utils/procedural-memory-extractor.d.ts.map +1 -0
  91. package/dist/shared/utils/procedural-memory-extractor.js +581 -0
  92. package/dist/shared/utils/procedural-memory-extractor.js.map +1 -0
  93. package/dist/shared/utils/relation-type-converter.d.ts +52 -0
  94. package/dist/shared/utils/relation-type-converter.d.ts.map +1 -0
  95. package/dist/shared/utils/relation-type-converter.js +106 -0
  96. package/dist/shared/utils/relation-type-converter.js.map +1 -0
  97. package/dist/shared/utils/type-param-validator.d.ts +31 -0
  98. package/dist/shared/utils/type-param-validator.d.ts.map +1 -1
  99. package/dist/shared/utils/type-param-validator.js +90 -2
  100. package/dist/shared/utils/type-param-validator.js.map +1 -1
  101. package/dist/tools/base-tool.d.ts.map +1 -1
  102. package/dist/tools/types.d.ts +4 -0
  103. package/dist/tools/types.d.ts.map +1 -1
  104. package/dist/tools/types.js +5 -0
  105. package/dist/tools/types.js.map +1 -1
  106. package/dist/workers/consolidation-score-worker.d.ts.map +1 -1
  107. package/dist/workers/consolidation-score-worker.js +0 -2
  108. package/dist/workers/consolidation-score-worker.js.map +1 -1
  109. package/package.json +3 -3
  110. package/scripts/auto-setup.js +1 -1
  111. /package/dist/{database → infrastructure/database/database}/migration/migrations/002-mirix-schema-expansion-core-memory.sql +0 -0
  112. /package/dist/{database → infrastructure/database/database}/migration/migrations/002-mirix-schema-expansion-knowledge-vault.sql +0 -0
  113. /package/dist/{database → infrastructure/database/database}/migration/migrations/002-mirix-schema-expansion-memory-item.sql +0 -0
  114. /package/dist/{database → infrastructure/database/database}/migration/migrations/002-mirix-schema-expansion-schema-version.sql +0 -0
  115. /package/dist/{database → infrastructure/database/database}/migration/migrations/003-consolidation-score-fields.sql +0 -0
  116. /package/dist/{database → infrastructure/database/database}/migration/migrations/005-relation-engine-schema.sql +0 -0
  117. /package/dist/{database → infrastructure/database/database}/migration/migrations/006-fts5-reflection-notes-migration-status.sql +0 -0
  118. /package/dist/{database → infrastructure/database/database}/migration/migrations/006-fts5-reflection-notes.sql +0 -0
@@ -14,6 +14,9 @@ import { KnowledgeVaultService } from '../services/knowledge-vault-service.js';
14
14
  import { validateTypeParam } from '../../../shared/utils/type-param-validator.js';
15
15
  import { mementoConfig } from '../../../shared/config/index.js';
16
16
  import { DatabaseUtils } from '../../../shared/utils/database.js';
17
+ import { MemoryNeighborService } from '../services/memory-neighbor-service.js';
18
+ import { getVectorSearchEngine } from '../../search/algorithms/vector-search-engine.js';
19
+ import { MemoryEmbeddingService } from '../services/memory-embedding-service.js';
17
20
  /**
18
21
  * Provider 필터 정규화 유틸리티
19
22
  * 빈 배열인 경우 undefined로 변환하여 모든 provider 검색을 의미
@@ -41,12 +44,25 @@ const RecallSchema = z.object({
41
44
  importance_min: z.number().min(0).max(1).optional(),
42
45
  importance_max: z.number().min(0).max(1).optional(),
43
46
  has_reflection_notes: z.boolean().optional(), // reflection_notes IS NOT NULL 필터링
47
+ // Procedural Memory Enhancement (v7.0) 필드
48
+ workflow_name: z.string().optional(),
49
+ skill_name: z.string().optional(),
50
+ match_trigger_conditions: z.boolean().optional().default(false),
51
+ context: z.record(z.any()).optional(), // 구조화된 컨텍스트 정보 (trigger_conditions 매칭용, 예: {tool_name, error_type, params})
52
+ trigger_context: z.record(z.any()).optional(), // context의 별칭 (하위 호환성)
53
+ return_format: z.enum(['full', 'steps_only']).optional().default('full'),
44
54
  limit: CommonSchemas.Limit,
45
55
  vector_weight: z.number().min(0).max(1).optional(),
46
56
  text_weight: z.number().min(0).max(1).optional(),
47
57
  enable_hybrid: z.boolean().optional(),
48
58
  include_metadata: z.boolean().optional(),
49
- provider_filter: z.array(z.enum(['tfidf', 'lightweight', 'minilm', 'openai', 'gemini'])).optional()
59
+ provider_filter: z.array(z.enum(['tfidf', 'lightweight', 'minilm', 'openai', 'gemini'])).optional(),
60
+ // 자동 앵커 설정 및 이웃 기억 포함 파라미터
61
+ auto_set_anchor: z.boolean().optional().default(false),
62
+ include_neighbors: z.boolean().optional().default(false),
63
+ neighbors_limit: z.number().min(1).max(10).optional().default(3),
64
+ neighbors_per_item: z.number().min(1).max(50).optional().default(5),
65
+ neighbors_similarity_threshold: z.number().min(0).max(1).optional().default(0.8)
50
66
  }).refine((data) => {
51
67
  // 조건부 필수 검증
52
68
  if (data.type === 'core' || data.type === 'vault') {
@@ -128,6 +144,26 @@ export class RecallTool extends BaseTool {
128
144
  type: 'boolean',
129
145
  description: 'reflection_notes가 있는 메모리만 조회 (true: IS NOT NULL, false: IS NULL, 선택사항)'
130
146
  },
147
+ // Procedural Memory Enhancement (v7.0) 필드
148
+ workflow_name: {
149
+ type: 'string',
150
+ description: '프로세스 이름으로 필터링 (선택사항)'
151
+ },
152
+ skill_name: {
153
+ type: 'string',
154
+ description: '기술/능력 이름으로 필터링 (선택사항)'
155
+ },
156
+ match_trigger_conditions: {
157
+ type: 'boolean',
158
+ default: false,
159
+ description: 'trigger_conditions 매칭 여부 (기본값: false)'
160
+ },
161
+ return_format: {
162
+ type: 'string',
163
+ enum: ['full', 'steps_only'],
164
+ default: 'full',
165
+ description: '반환 형식 선택: full (모든 필드), steps_only (steps만 반환)'
166
+ },
131
167
  limit: {
132
168
  type: 'number',
133
169
  minimum: 1,
@@ -163,6 +199,37 @@ export class RecallTool extends BaseTool {
163
199
  type: 'array',
164
200
  items: { type: 'string', enum: ['tfidf', 'lightweight', 'minilm', 'openai', 'gemini'] },
165
201
  description: '검색할 임베딩 provider 필터 (선택사항, 미지정 시 모든 provider 검색)'
202
+ },
203
+ auto_set_anchor: {
204
+ type: 'boolean',
205
+ default: false,
206
+ description: '가장 관련성 높은 기억(첫 번째 결과)을 슬롯 A에 자동으로 앵커로 설정 (기본값: false)'
207
+ },
208
+ include_neighbors: {
209
+ type: 'boolean',
210
+ default: false,
211
+ description: '검색 결과의 상위 항목에 대해 이웃 기억을 자동으로 포함 (기본값: false)'
212
+ },
213
+ neighbors_limit: {
214
+ type: 'number',
215
+ minimum: 1,
216
+ maximum: 10,
217
+ default: 3,
218
+ description: '이웃 기억을 포함할 상위 결과의 개수 (각 결과당 이웃 개수는 neighbors_per_item으로 제어, 기본값: 3)'
219
+ },
220
+ neighbors_per_item: {
221
+ type: 'number',
222
+ minimum: 1,
223
+ maximum: 50,
224
+ default: 5,
225
+ description: '각 검색 결과 항목당 조회할 이웃 기억의 최대 개수 (기본값: 5)'
226
+ },
227
+ neighbors_similarity_threshold: {
228
+ type: 'number',
229
+ minimum: 0,
230
+ maximum: 1,
231
+ default: 0.8,
232
+ description: '이웃 기억 조회 시 유사도 임계값 (이 값 이상인 기억만 반환, 기본값: 0.8)'
166
233
  }
167
234
  },
168
235
  required: [] // 조건부 필수는 런타임 검증 (RecallSchema.refine()에서 처리)
@@ -173,7 +240,9 @@ export class RecallTool extends BaseTool {
173
240
  this.logInfo('Recall 도구 호출됨', { params });
174
241
  try {
175
242
  // 파라미터 검증 및 파싱
176
- const { query, type, key, agent_id, memory_types, tags, privacy_scope, time_from, time_to, pinned, importance_min, importance_max, limit, vector_weight, text_weight, enable_hybrid, include_metadata, provider_filter } = RecallSchema.parse(params);
243
+ const { query, type, key, agent_id, memory_types, tags, privacy_scope, time_from, time_to, pinned, importance_min, importance_max, workflow_name, skill_name, match_trigger_conditions, context: triggerContext, trigger_context, return_format, limit, vector_weight, text_weight, enable_hybrid, include_metadata, provider_filter, auto_set_anchor, include_neighbors, neighbors_limit, neighbors_per_item, neighbors_similarity_threshold } = RecallSchema.parse(params);
244
+ // trigger_context가 제공되면 context로 사용 (하위 호환성)
245
+ const actualTriggerContext = triggerContext || trigger_context;
177
246
  // type 파라미터 롤아웃 모드 검증
178
247
  // PRD 요구사항: Phase 1/2에서는 type 파라미터가 없으면 항상 경고/Deprecated 메시지를 띄워야 함
179
248
  // memory_types만 있어도 경고를 띄워야 하므로, type이 없으면 항상 검증 수행
@@ -219,7 +288,7 @@ export class RecallTool extends BaseTool {
219
288
  });
220
289
  // 데이터베이스 연결 확인
221
290
  this.validateDatabase(context);
222
- const startTime = Date.now();
291
+ const searchStartTime = Date.now();
223
292
  const agentId = agent_id || 'default';
224
293
  // type 파라미터에 따른 분기 처리
225
294
  if (validatedType === 'core') {
@@ -244,7 +313,7 @@ export class RecallTool extends BaseTool {
244
313
  // 전체 Core Memory 조회
245
314
  records = await coreMemoryService.findByAgentId(agentId);
246
315
  }
247
- const executionTime = Date.now() - startTime;
316
+ const executionTime = Date.now() - searchStartTime;
248
317
  const processedResults = records.map(record => ({
249
318
  memory_id: record.core_id,
250
319
  type: 'core',
@@ -282,7 +351,7 @@ export class RecallTool extends BaseTool {
282
351
  // 전체 Vault 조회 (활성 버전만)
283
352
  records = await knowledgeVaultService.findActiveByAgentId(agentId);
284
353
  }
285
- const executionTime = Date.now() - startTime;
354
+ const executionTime = Date.now() - searchStartTime;
286
355
  const processedResults = records.map(record => ({
287
356
  memory_id: record.vault_id,
288
357
  type: 'vault',
@@ -321,8 +390,24 @@ export class RecallTool extends BaseTool {
321
390
  });
322
391
  }
323
392
  // memory_types 배열 전처리 ('core'/'vault' 제거)
324
- // 원래 type 파라미터가 제공되었는지 확인하여 fallback 동작 보장
325
- let filteredMemoryTypes = originalTypeProvided ? (validatedType ? [validatedType] : undefined) : memory_types;
393
+ // validatedType이 존재하면 항상 [validatedType]로 시작 (기본값 포함)
394
+ // originalTypeProvided false이고 memory_types가 제공되면 고려하되, validatedType 우선
395
+ let filteredMemoryTypes;
396
+ if (validatedType) {
397
+ // validatedType이 있으면 항상 사용 (기본값이든 명시적 값이든)
398
+ filteredMemoryTypes = [validatedType];
399
+ // originalTypeProvided가 false이고 memory_types도 제공되면 경고
400
+ if (!originalTypeProvided && memory_types && memory_types.length > 0) {
401
+ this.logWarning('type 파라미터가 미지정되어 기본값이 적용되었지만, memory_types도 제공되었습니다. 기본 타입을 우선 적용하고 memory_types는 무시합니다.', {
402
+ default_type: validatedType,
403
+ memory_types
404
+ });
405
+ }
406
+ }
407
+ else {
408
+ // validatedType이 없으면 memory_types 사용
409
+ filteredMemoryTypes = memory_types;
410
+ }
326
411
  if (filteredMemoryTypes && filteredMemoryTypes.length > 0) {
327
412
  const invalidTypes = filteredMemoryTypes.filter(t => t === 'core' || t === 'vault');
328
413
  if (invalidTypes.length > 0) {
@@ -357,7 +442,10 @@ export class RecallTool extends BaseTool {
357
442
  pinned,
358
443
  importance_min,
359
444
  importance_max,
360
- has_reflection_notes: params.has_reflection_notes
445
+ has_reflection_notes: params.has_reflection_notes,
446
+ // Procedural Memory Enhancement (v7.0) 필터
447
+ workflow_name,
448
+ skill_name
361
449
  };
362
450
  // 검색 옵션 설정
363
451
  const vectorWeight = vector_weight ?? 0.6;
@@ -386,7 +474,9 @@ export class RecallTool extends BaseTool {
386
474
  limit,
387
475
  vectorWeight: normalizedVectorWeight,
388
476
  textWeight: normalizedTextWeight,
389
- provider_filter: providerFilter
477
+ provider_filter: providerFilter,
478
+ match_trigger_conditions: match_trigger_conditions,
479
+ context: actualTriggerContext // 구조화된 컨텍스트 정보 전달
390
480
  });
391
481
  }
392
482
  else {
@@ -406,20 +496,52 @@ export class RecallTool extends BaseTool {
406
496
  this.logError(searchError, '검색 실행 중 오류', { query, enableHybrid });
407
497
  throw new Error(`검색 실행 실패: ${searchError.message}`);
408
498
  }
409
- const executionTime = Date.now() - startTime;
499
+ const executionTime = Date.now() - searchStartTime;
410
500
  // 검색 결과 가져오기
411
- const searchItems = searchResult?.items || [];
501
+ let searchItems = searchResult?.items || [];
502
+ // trigger_conditions 매칭 필터링 (match_trigger_conditions=true일 때)
503
+ if (match_trigger_conditions && searchItems.length > 0) {
504
+ searchItems = this.filterByTriggerConditions(searchItems, query, actualTriggerContext);
505
+ }
412
506
  // Consolidation Score System 업데이트 (기능 플래그 확인)
413
507
  if (mementoConfig.consolidationScoreEnabled && context.services.consolidationScoreService && searchItems.length > 0) {
414
508
  await this.updateConsolidationScoreMetadata(context.db, context.services.consolidationScoreService, context.services.writeCoalescingManager, searchItems);
415
509
  }
416
510
  // 결과 후처리 - searchResult가 undefined인 경우 처리
417
- const processedResults = this.processSearchResults(searchItems, includeMetadata);
511
+ const processedResults = this.processSearchResults(searchItems, includeMetadata, return_format);
512
+ // 자동 앵커 설정 처리 (auto_set_anchor=true이고 검색 결과가 있을 때)
513
+ let anchorSetResult = null;
514
+ if (auto_set_anchor && searchItems.length > 0) {
515
+ anchorSetResult = await this.handleAutoSetAnchor(searchItems, agentId, context);
516
+ }
517
+ // 자동 이웃 기억 포함 처리 (include_neighbors=true이고 검색 결과가 있을 때)
518
+ let neighborsResults = [];
519
+ if (include_neighbors && searchItems.length > 0) {
520
+ neighborsResults = await this.handleIncludeNeighbors(searchItems, neighbors_limit, neighbors_per_item, neighbors_similarity_threshold, context);
521
+ // 검색 결과 항목에 neighbors 필드 추가
522
+ // neighbors_limit보다 많은 결과는 neighbors 필드 없음 (handleIncludeNeighbors가 상위 neighbors_limit개만 처리)
523
+ for (let i = 0; i < Math.min(neighborsResults.length, processedResults.length); i++) {
524
+ processedResults[i].neighbors = neighborsResults[i];
525
+ }
526
+ }
418
527
  this.logInfo('검색 완료', {
419
528
  resultCount: processedResults.length,
420
529
  executionTime,
421
530
  searchType: enableHybrid ? 'hybrid' : 'text'
422
531
  });
532
+ // 메타데이터 구성 (앵커 설정 결과 포함)
533
+ const metadata = {
534
+ anchor_set: anchorSetResult?.anchor_set || null
535
+ };
536
+ // 앵커 설정 실패 시
537
+ if (anchorSetResult && anchorSetResult.error) {
538
+ metadata.anchor_set_error = true;
539
+ }
540
+ // 앵커 설정 건너뜀 시
541
+ if (anchorSetResult && anchorSetResult.skipped) {
542
+ metadata.anchor_set_skipped = true;
543
+ metadata.anchor_set_skipped_reason = anchorSetResult.skipped_reason;
544
+ }
423
545
  return this.createSuccessResult({
424
546
  items: processedResults,
425
547
  total_count: searchResult?.total_count || processedResults.length,
@@ -431,7 +553,8 @@ export class RecallTool extends BaseTool {
431
553
  vector_weight: normalizedVectorWeight,
432
554
  text_weight: normalizedTextWeight,
433
555
  enable_hybrid: enableHybrid
434
- }
556
+ },
557
+ metadata
435
558
  });
436
559
  }
437
560
  }
@@ -455,10 +578,91 @@ export class RecallTool extends BaseTool {
455
578
  throw error;
456
579
  }
457
580
  }
581
+ /**
582
+ * trigger_conditions로 필터링
583
+ * match_trigger_conditions=true일 때, 현재 컨텍스트와 trigger_conditions가 매칭되는 항목만 반환
584
+ *
585
+ * PRD 요구사항: 구조화된 컨텍스트(예: tool_name, error_type, params)와 JSON 매칭
586
+ * 구조화된 컨텍스트가 제공되면 이를 우선 사용하고, 없으면 쿼리 텍스트를 사용
587
+ *
588
+ * @param items 검색 결과 항목 배열
589
+ * @param query 검색 쿼리 (컨텍스트로 사용, fallback)
590
+ * @param context 구조화된 컨텍스트 정보 (우선 사용)
591
+ * @returns 필터링된 항목 배열
592
+ */
593
+ filterByTriggerConditions(items, query, triggerContext) {
594
+ const queryText = query?.toLowerCase() || '';
595
+ return items.filter(item => {
596
+ // trigger_conditions가 없는 항목은 제외
597
+ if (!item.trigger_conditions) {
598
+ return false;
599
+ }
600
+ try {
601
+ // JSON 파싱 시도
602
+ const parsed = typeof item.trigger_conditions === 'string'
603
+ ? JSON.parse(item.trigger_conditions)
604
+ : item.trigger_conditions;
605
+ // 객체인지 확인 (배열이나 null이 아닌 경우)
606
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
607
+ return false;
608
+ }
609
+ // 구조화된 컨텍스트가 제공된 경우: 키-값 기반 정확 매칭
610
+ // 모든 키/값 쌍이 매칭되어야 함 (첫 번째 키만 맞으면 통과하는 문제 수정)
611
+ if (triggerContext && Object.keys(triggerContext).length > 0) {
612
+ // trigger_conditions의 모든 키-값 쌍이 컨텍스트와 매칭되는지 확인
613
+ for (const [key, value] of Object.entries(parsed)) {
614
+ const contextValue = triggerContext[key];
615
+ // trigger_conditions에 있는 키가 컨텍스트에 없으면 매칭 실패
616
+ if (contextValue === undefined) {
617
+ return false;
618
+ }
619
+ // 값이 객체인 경우 재귀적으로 비교
620
+ if (typeof value === 'object' && typeof contextValue === 'object' && value !== null && contextValue !== null) {
621
+ // 중첩 객체 매칭: context의 값이 trigger_conditions의 값과 부분적으로 일치하는지 확인
622
+ const valueStr = JSON.stringify(value).toLowerCase();
623
+ const contextStr = JSON.stringify(contextValue).toLowerCase();
624
+ if (!(valueStr.includes(contextStr) || contextStr.includes(valueStr))) {
625
+ // 하나라도 매칭되지 않으면 실패
626
+ return false;
627
+ }
628
+ }
629
+ else {
630
+ // 단순 값 매칭: 문자열로 변환하여 비교
631
+ const valueStr = String(value).toLowerCase();
632
+ const contextStr = String(contextValue).toLowerCase();
633
+ if (!(valueStr === contextStr || valueStr.includes(contextStr) || contextStr.includes(valueStr))) {
634
+ // 하나라도 매칭되지 않으면 실패
635
+ return false;
636
+ }
637
+ }
638
+ }
639
+ // 모든 키/값 쌍이 매칭됨
640
+ return true;
641
+ }
642
+ // 구조화된 컨텍스트가 없는 경우: 쿼리 텍스트 기반 매칭 (fallback)
643
+ if (queryText) {
644
+ // 키 매칭: tool_name, error_type, params 등 구조화된 필드명과 매칭
645
+ const triggerKeys = Object.keys(parsed).map(k => k.toLowerCase());
646
+ const triggerValues = Object.values(parsed).map(v => String(v).toLowerCase());
647
+ // 키 또는 값 중 하나라도 쿼리와 매칭되면 통과
648
+ const keyMatch = triggerKeys.some(k => k.includes(queryText) || queryText.includes(k));
649
+ const valueMatch = triggerValues.some(v => v.includes(queryText) || queryText.includes(v));
650
+ return keyMatch || valueMatch;
651
+ }
652
+ // 쿼리와 컨텍스트가 모두 없으면 매칭 기준이 없으므로 필터링
653
+ // PRD: "현재 컨텍스트와 매칭" 요구사항 - 매칭 기준이 없으면 통과하지 않음
654
+ return false;
655
+ }
656
+ catch (error) {
657
+ // JSON 파싱 실패 시 제외
658
+ return false;
659
+ }
660
+ });
661
+ }
458
662
  /**
459
663
  * 검색 결과 후처리
460
664
  */
461
- processSearchResults(items, includeMetadata) {
665
+ processSearchResults(items, includeMetadata, returnFormat = 'full') {
462
666
  return items.map(item => {
463
667
  const processed = {
464
668
  memory_id: item.id || item.memory_id, // 통일된 필드명 사용
@@ -491,6 +695,10 @@ export class RecallTool extends BaseTool {
491
695
  if (item.type === 'procedural') {
492
696
  processed.task_goal = item.task_goal || null;
493
697
  processed.steps = item.steps || null;
698
+ // Procedural Memory Enhancement (v7.0) 필드 추가
699
+ processed.workflow_name = item.workflow_name || null;
700
+ processed.skill_name = item.skill_name || null;
701
+ processed.trigger_conditions = item.trigger_conditions || null;
494
702
  // reflection_notes 필드 추가 (JSON 파싱)
495
703
  if (item.reflection_notes) {
496
704
  try {
@@ -507,6 +715,15 @@ export class RecallTool extends BaseTool {
507
715
  else {
508
716
  processed.reflection_notes = null;
509
717
  }
718
+ // return_format='steps_only'일 때 steps만 반환
719
+ if (returnFormat === 'steps_only') {
720
+ // steps만 포함하고 나머지 필드는 제거
721
+ return {
722
+ memory_id: processed.memory_id,
723
+ id: processed.id,
724
+ steps: processed.steps
725
+ };
726
+ }
510
727
  }
511
728
  if (item.textScore !== undefined) {
512
729
  processed.text_score = item.textScore;
@@ -561,6 +778,263 @@ export class RecallTool extends BaseTool {
561
778
  }
562
779
  return applied;
563
780
  }
781
+ /**
782
+ * 자동 앵커 설정 처리
783
+ * 가장 관련성 높은 기억(첫 번째 결과)을 슬롯 A에 앵커로 설정
784
+ *
785
+ * @param searchItems - 검색 결과 항목 배열
786
+ * @param agentId - 에이전트 ID
787
+ * @param context - 도구 컨텍스트
788
+ * @returns 앵커 설정 결과 (성공/실패/건너뜀 상태 포함)
789
+ */
790
+ async handleAutoSetAnchor(searchItems, agentId, context) {
791
+ // 검색 결과가 없으면 앵커 설정 불가
792
+ if (!searchItems || searchItems.length === 0) {
793
+ return {
794
+ success: false,
795
+ anchor_set: null
796
+ };
797
+ }
798
+ // 첫 번째 결과의 memory_id 가져오기
799
+ const topMemory = searchItems[0];
800
+ const memoryId = topMemory.id || topMemory.memory_id;
801
+ if (!memoryId) {
802
+ this.logWarning('검색 결과에 memory_id가 없어 앵커 설정을 건너뜁니다', { topMemory });
803
+ return {
804
+ success: false,
805
+ anchor_set: null,
806
+ error: true
807
+ };
808
+ }
809
+ // AnchorManager 서비스 확인
810
+ if (!context.services.anchorManager) {
811
+ this.logWarning('AnchorManager 서비스가 없어 앵커 설정을 건너뜁니다');
812
+ return {
813
+ success: false,
814
+ anchor_set: null,
815
+ error: true
816
+ };
817
+ }
818
+ try {
819
+ // 슬롯 A의 앵커 조회 및 pinned 상태 확인 (memory_item 테이블과 조인)
820
+ const slotAAnchor = await context.services.anchorManager.getAnchor(agentId, 'A');
821
+ if (slotAAnchor && typeof slotAAnchor === 'object' && 'memory_id' in slotAAnchor) {
822
+ // pinned 상태 확인 (memory_item 테이블과 조인)
823
+ const anchorMemory = context.db.prepare(`
824
+ SELECT pinned FROM memory_item WHERE id = ?
825
+ `).get(slotAAnchor.memory_id);
826
+ const isPinned = anchorMemory && (anchorMemory.pinned === 1 || anchorMemory.pinned === true);
827
+ // 슬롯 A에 pinned 앵커가 있으면 건너뛰기 (보호 정책)
828
+ if (isPinned) {
829
+ this.logInfo('슬롯 A에 pinned 앵커가 있어 앵커 설정을 건너뜁니다', {
830
+ agent_id: agentId,
831
+ existing_memory_id: slotAAnchor.memory_id
832
+ });
833
+ return {
834
+ success: false,
835
+ anchor_set: null,
836
+ skipped: true,
837
+ skipped_reason: 'pinned_anchor_protected'
838
+ };
839
+ }
840
+ // 슬롯 A에 일반 앵커가 있으면 슬롯 B로 이동
841
+ const slotBAnchor = await context.services.anchorManager.getAnchor(agentId, 'B');
842
+ if (slotBAnchor && typeof slotBAnchor === 'object' && 'memory_id' in slotBAnchor) {
843
+ // 슬롯 B의 pinned 상태 확인
844
+ const slotBMemory = context.db.prepare(`
845
+ SELECT pinned FROM memory_item WHERE id = ?
846
+ `).get(slotBAnchor.memory_id);
847
+ const slotBIsPinned = slotBMemory && (slotBMemory.pinned === 1 || slotBMemory.pinned === true);
848
+ if (slotBIsPinned) {
849
+ this.logWarning('슬롯 B의 pinned 앵커가 덮어써집니다', {
850
+ agent_id: agentId,
851
+ old_memory_id: slotBAnchor.memory_id,
852
+ new_memory_id: slotAAnchor.memory_id
853
+ });
854
+ }
855
+ // 슬롯 B에 앵커가 있으면 슬롯 C로 이동
856
+ const slotCAnchor = await context.services.anchorManager.getAnchor(agentId, 'C');
857
+ if (slotCAnchor && typeof slotCAnchor === 'object' && 'memory_id' in slotCAnchor) {
858
+ // 슬롯 C의 pinned 상태 확인
859
+ const slotCMemory = context.db.prepare(`
860
+ SELECT pinned FROM memory_item WHERE id = ?
861
+ `).get(slotCAnchor.memory_id);
862
+ const slotCIsPinned = slotCMemory && (slotCMemory.pinned === 1 || slotCMemory.pinned === true);
863
+ if (slotCIsPinned) {
864
+ this.logWarning('슬롯 C의 pinned 앵커가 제거됩니다', {
865
+ agent_id: agentId,
866
+ old_memory_id: slotCAnchor.memory_id
867
+ });
868
+ }
869
+ // 슬롯 C에 앵커가 있으면 제거 (pinned 여부와 관계없이 회전 규칙에 따라 제거)
870
+ await context.services.anchorManager.clearAnchor(agentId, 'C');
871
+ }
872
+ // 슬롯 B의 기존 앵커를 슬롯 C로 이동 (슬롯 B를 비우기 위해)
873
+ // PRD: 슬롯 B/C의 pinned 앵커도 덮어쓰고 A→B→C→제거 순으로 회전
874
+ // 먼저 슬롯 B를 제거한 후 슬롯 C에 설정
875
+ const slotBMemoryId = slotBAnchor.memory_id;
876
+ await context.services.anchorManager.clearAnchor(agentId, 'B');
877
+ await context.services.anchorManager.setAnchor(agentId, slotBMemoryId, 'C');
878
+ }
879
+ // 슬롯 A의 앵커를 슬롯 B로 이동
880
+ // 먼저 슬롯 A를 제거한 후 슬롯 B에 설정
881
+ const slotAMemoryId = slotAAnchor.memory_id;
882
+ await context.services.anchorManager.clearAnchor(agentId, 'A');
883
+ await context.services.anchorManager.setAnchor(agentId, slotAMemoryId, 'B');
884
+ }
885
+ // 새로운 기억을 슬롯 A에 설정
886
+ await context.services.anchorManager.setAnchor(agentId, memoryId, 'A');
887
+ this.logInfo('앵커가 자동으로 설정되었습니다', {
888
+ agent_id: agentId,
889
+ memory_id: memoryId,
890
+ slot: 'A'
891
+ });
892
+ return {
893
+ success: true,
894
+ anchor_set: {
895
+ memory_id: memoryId,
896
+ slot: 'A',
897
+ agent_id: agentId
898
+ }
899
+ };
900
+ }
901
+ catch (error) {
902
+ // 앵커 설정 실패 시 경고만 로그하고 검색 결과는 정상 반환
903
+ this.logError(error, '앵커 자동 설정 실패', {
904
+ agent_id: agentId,
905
+ memory_id: memoryId
906
+ });
907
+ return {
908
+ success: false,
909
+ anchor_set: null,
910
+ error: true
911
+ };
912
+ }
913
+ }
914
+ /**
915
+ * 자동 이웃 기억 포함 처리
916
+ * 검색 결과의 상위 항목에 대해 이웃 기억을 자동으로 포함
917
+ *
918
+ * @param searchItems - 검색 결과 항목 배열
919
+ * @param neighborsLimit - 이웃 기억을 포함할 상위 결과의 개수
920
+ * @param neighborsPerItem - 각 검색 결과 항목당 조회할 이웃 기억의 최대 개수
921
+ * @param neighborsSimilarityThreshold - 이웃 기억 조회 시 유사도 임계값
922
+ * @param context - 도구 컨텍스트
923
+ * @returns 각 검색 결과 항목에 대한 이웃 기억 배열 (순서 보존)
924
+ */
925
+ async handleIncludeNeighbors(searchItems, neighborsLimit, neighborsPerItem, neighborsSimilarityThreshold, context) {
926
+ // 검색 결과가 없으면 빈 배열 반환
927
+ if (!searchItems || searchItems.length === 0) {
928
+ return [];
929
+ }
930
+ // 상위 neighbors_limit개 결과 추출 (검색 결과 개수보다 작으면 검색 결과 개수로 제한)
931
+ const topResults = searchItems.slice(0, Math.min(neighborsLimit, searchItems.length));
932
+ // MemoryNeighborService 인스턴스 생성
933
+ let neighborService;
934
+ try {
935
+ const vectorSearchEngine = getVectorSearchEngine();
936
+ const embeddingService = context.services.embeddingService || new MemoryEmbeddingService();
937
+ neighborService = new MemoryNeighborService(vectorSearchEngine, embeddingService);
938
+ neighborService.setDatabase(context.db);
939
+ }
940
+ catch (error) {
941
+ this.logError(error, 'MemoryNeighborService 초기화 실패', {});
942
+ // 서비스 초기화 실패 시 빈 배열 반환 (각 요소가 독립적인 배열 인스턴스)
943
+ return Array.from({ length: topResults.length }, () => []);
944
+ }
945
+ // 각 상위 결과에 대해 이웃 기억 조회를 병렬 처리
946
+ const neighborPromises = topResults.map(async (item, index) => {
947
+ const memoryId = item.id || item.memory_id;
948
+ if (!memoryId) {
949
+ this.logWarning('검색 결과에 memory_id가 없어 이웃 기억 조회를 건너뜁니다', { item });
950
+ return { index, neighbors: [] };
951
+ }
952
+ try {
953
+ // 개별 이웃 기억 조회에 타임아웃 적용 (각 조회당 최대 2초)
954
+ const timeoutPromise = new Promise((_, reject) => {
955
+ setTimeout(() => reject(new Error('Timeout')), 2000);
956
+ });
957
+ const neighborPromise = neighborService.getNeighbors(memoryId, {
958
+ limit: neighborsPerItem,
959
+ similarity_threshold: neighborsSimilarityThreshold
960
+ }).then(result => ({
961
+ index,
962
+ neighbors: result.neighbors
963
+ }));
964
+ const result = await Promise.race([neighborPromise, timeoutPromise]);
965
+ return result;
966
+ }
967
+ catch (error) {
968
+ // 타임아웃 또는 에러 발생 시 빈 배열 반환
969
+ if (error instanceof Error && error.message === 'Timeout') {
970
+ this.logWarning('이웃 기억 조회 타임아웃', { memoryId, index });
971
+ }
972
+ else {
973
+ this.logError(error, '이웃 기억 조회 실패', { memoryId, index });
974
+ }
975
+ return { index, neighbors: [] };
976
+ }
977
+ });
978
+ // 전체 요청 타임아웃 적용 (2.5초, 부분 성공 결과 반환)
979
+ // 각 promise의 완료 상태를 추적하여 타임아웃 시 즉시 완료된 것만 반환
980
+ const completedResults = new Map();
981
+ // 각 promise에 대해 완료 시 결과를 저장
982
+ neighborPromises.forEach((promise, idx) => {
983
+ promise
984
+ .then(result => {
985
+ completedResults.set(idx, result);
986
+ })
987
+ .catch(() => {
988
+ // 에러는 무시하고 빈 배열로 처리
989
+ completedResults.set(idx, { index: idx, neighbors: [] });
990
+ });
991
+ });
992
+ let timeoutId = null;
993
+ const timeoutPromise = new Promise((resolve) => {
994
+ timeoutId = setTimeout(() => {
995
+ // 타임아웃 시 현재까지 완료된 결과만 즉시 반환 (Promise.allSettled를 기다리지 않음)
996
+ const partialResults = [];
997
+ // 완료된 결과 수집
998
+ for (let i = 0; i < topResults.length; i++) {
999
+ if (completedResults.has(i)) {
1000
+ partialResults.push(completedResults.get(i));
1001
+ }
1002
+ else {
1003
+ // 완료되지 않은 항목은 빈 배열로 채움
1004
+ partialResults.push({ index: i, neighbors: [] });
1005
+ }
1006
+ }
1007
+ // 인덱스 순서로 정렬하여 반환
1008
+ resolve(partialResults.sort((a, b) => a.index - b.index));
1009
+ }, 2500); // 전체 타임아웃: 2.5초
1010
+ });
1011
+ try {
1012
+ const allNeighbors = await Promise.race([
1013
+ Promise.all(neighborPromises),
1014
+ timeoutPromise
1015
+ ]);
1016
+ // 타임아웃 취소
1017
+ if (timeoutId)
1018
+ clearTimeout(timeoutId);
1019
+ // 결과를 원래 순서로 정렬 (인덱스 기준)
1020
+ const sortedNeighbors = allNeighbors
1021
+ .sort((a, b) => a.index - b.index)
1022
+ .map(r => r.neighbors);
1023
+ return sortedNeighbors;
1024
+ }
1025
+ catch (error) {
1026
+ // 타임아웃 취소
1027
+ if (timeoutId)
1028
+ clearTimeout(timeoutId);
1029
+ // 타임아웃 시에도 부분 완료 결과는 반환됨 (timeoutPromise에서 처리)
1030
+ // 완료된 결과만 반환
1031
+ const settledResults = await Promise.allSettled(neighborPromises);
1032
+ return settledResults.map((r, idx) => r.status === 'fulfilled'
1033
+ ? r.value.neighbors
1034
+ : [] // 실패한 항목은 빈 배열
1035
+ );
1036
+ }
1037
+ }
564
1038
  /**
565
1039
  * 검색 쿼리 검증
566
1040
  */