byterover-cli 3.0.0 → 3.1.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.
- package/dist/agent/core/domain/tools/constants.d.ts +1 -0
- package/dist/agent/core/domain/tools/constants.js +1 -0
- package/dist/agent/core/interfaces/cipher-services.d.ts +8 -0
- package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/agent-error-codes.d.ts +0 -1
- package/dist/agent/infra/agent/agent-error-codes.js +0 -1
- package/dist/agent/infra/agent/agent-error.d.ts +0 -1
- package/dist/agent/infra/agent/agent-error.js +0 -1
- package/dist/agent/infra/agent/agent-state-manager.d.ts +1 -3
- package/dist/agent/infra/agent/agent-state-manager.js +1 -3
- package/dist/agent/infra/agent/base-agent.d.ts +1 -1
- package/dist/agent/infra/agent/base-agent.js +1 -1
- package/dist/agent/infra/agent/cipher-agent.d.ts +15 -1
- package/dist/agent/infra/agent/cipher-agent.js +188 -3
- package/dist/agent/infra/agent/index.d.ts +1 -1
- package/dist/agent/infra/agent/index.js +1 -1
- package/dist/agent/infra/agent/service-initializer.d.ts +3 -3
- package/dist/agent/infra/agent/service-initializer.js +14 -8
- package/dist/agent/infra/agent/types.d.ts +0 -1
- package/dist/agent/infra/file-system/file-system-service.js +6 -5
- package/dist/agent/infra/folder-pack/folder-pack-service.d.ts +1 -0
- package/dist/agent/infra/folder-pack/folder-pack-service.js +29 -15
- package/dist/agent/infra/llm/providers/openai.js +12 -0
- package/dist/agent/infra/llm/stream-to-text.d.ts +7 -0
- package/dist/agent/infra/llm/stream-to-text.js +14 -0
- package/dist/agent/infra/map/abstract-generator.d.ts +22 -0
- package/dist/agent/infra/map/abstract-generator.js +67 -0
- package/dist/agent/infra/map/abstract-queue.d.ts +67 -0
- package/dist/agent/infra/map/abstract-queue.js +218 -0
- package/dist/agent/infra/memory/memory-deduplicator.d.ts +44 -0
- package/dist/agent/infra/memory/memory-deduplicator.js +88 -0
- package/dist/agent/infra/memory/memory-manager.d.ts +1 -0
- package/dist/agent/infra/memory/memory-manager.js +6 -5
- package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
- package/dist/agent/infra/sandbox/curate-service.js +6 -7
- package/dist/agent/infra/sandbox/local-sandbox.d.ts +5 -0
- package/dist/agent/infra/sandbox/local-sandbox.js +57 -1
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +3 -1
- package/dist/agent/infra/session/session-compressor.d.ts +43 -0
- package/dist/agent/infra/session/session-compressor.js +296 -0
- package/dist/agent/infra/session/session-manager.d.ts +7 -0
- package/dist/agent/infra/session/session-manager.js +9 -0
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +3 -2
- package/dist/agent/infra/tools/implementations/curate-tool.js +54 -27
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +3 -3
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +34 -7
- package/dist/agent/infra/tools/implementations/ingest-resource-tool.d.ts +17 -0
- package/dist/agent/infra/tools/implementations/ingest-resource-tool.js +224 -0
- package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +8 -0
- package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +1 -1
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +207 -34
- package/dist/agent/infra/tools/implementations/search-knowledge-tool.js +2 -2
- package/dist/agent/infra/tools/tool-provider.js +1 -0
- package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
- package/dist/agent/infra/tools/tool-registry.js +15 -4
- package/dist/server/constants.d.ts +2 -0
- package/dist/server/constants.js +2 -0
- package/dist/server/core/domain/knowledge/memory-scoring.d.ts +3 -3
- package/dist/server/core/domain/knowledge/memory-scoring.js +5 -5
- package/dist/server/core/domain/knowledge/summary-types.d.ts +4 -0
- package/dist/server/core/domain/transport/schemas.d.ts +10 -10
- package/dist/server/infra/context-tree/derived-artifact.js +5 -1
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +2 -1
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +43 -7
- package/dist/server/infra/context-tree/file-context-tree-summary-service.js +20 -2
- package/dist/server/infra/executor/curate-executor.js +2 -1
- package/dist/server/infra/executor/folder-pack-executor.js +72 -2
- package/dist/server/infra/executor/query-executor.js +11 -3
- package/dist/server/infra/transport/handlers/status-handler.js +10 -0
- package/dist/server/utils/curate-result-parser.d.ts +4 -4
- package/dist/shared/transport/types/dto.d.ts +7 -0
- package/oclif.manifest.json +1 -1
- package/package.json +10 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import MiniSearch from 'minisearch';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { removeStopwords } from 'stopword';
|
|
4
|
-
import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, SUMMARY_INDEX_FILE } from '../../../../server/constants.js';
|
|
5
|
+
import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, OVERVIEW_EXTENSION, SUMMARY_INDEX_FILE } from '../../../../server/constants.js';
|
|
5
6
|
import { parseFrontmatterScoring, updateScoringInContent, } from '../../../../server/core/domain/knowledge/markdown-writer.js';
|
|
6
7
|
import { applyDecay, applyDefaultScoring, compoundScore, determineTier, recordAccessHits, } from '../../../../server/core/domain/knowledge/memory-scoring.js';
|
|
7
8
|
import { isArchiveStub, isDerivedArtifact } from '../../../../server/infra/context-tree/derived-artifact.js';
|
|
@@ -13,9 +14,9 @@ const DEFAULT_CACHE_TTL_MS = 5000;
|
|
|
13
14
|
/** Bump when MINISEARCH_OPTIONS fields/boost change to invalidate cached indexes */
|
|
14
15
|
const INDEX_SCHEMA_VERSION = 4;
|
|
15
16
|
/** Only include results whose normalized score is at least this fraction of the top result's score */
|
|
16
|
-
const SCORE_GAP_RATIO = 0.
|
|
17
|
+
const SCORE_GAP_RATIO = 0.7;
|
|
17
18
|
/** Minimum normalized score for the top result. Below this, the query is considered out-of-domain */
|
|
18
|
-
const MINIMUM_RELEVANCE_SCORE = 0.
|
|
19
|
+
const MINIMUM_RELEVANCE_SCORE = 0.45;
|
|
19
20
|
/** Normalized score threshold above which results are trusted despite unmatched query terms */
|
|
20
21
|
const UNMATCHED_TERM_SCORE_THRESHOLD = 0.85;
|
|
21
22
|
/** Minimum query term length to consider "significant" for OOD term-based detection */
|
|
@@ -33,6 +34,59 @@ const CHUNK_OVERLAP_CHARS = 120;
|
|
|
33
34
|
function normalizeScore(rawScore) {
|
|
34
35
|
return rawScore / (1 + rawScore);
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Propagate BM25 scores upward to parent domain/topic nodes.
|
|
39
|
+
*
|
|
40
|
+
* For each matched result, walks the parent chain and computes a decayed boost
|
|
41
|
+
* (score * propagationFactor per level). New summary entries are added for
|
|
42
|
+
* parent nodes that have a _index.md in summaryMap but are not already in results.
|
|
43
|
+
*
|
|
44
|
+
* @param results - Already-enriched search results (gap-ratio filtered)
|
|
45
|
+
* @param symbolTree - Symbol tree for parent-chain traversal
|
|
46
|
+
* @param summaryMap - Map of _index.md file paths → SummaryDocLike (for excerpt/metadata)
|
|
47
|
+
* @param propagationFactor - Score multiplier per level up (default 0.55)
|
|
48
|
+
* @returns New parent entries only — caller merges and re-sorts
|
|
49
|
+
*/
|
|
50
|
+
function propagateScoresToParents(results, symbolTree, summaryMap, documentMap, propagationFactor = 0.55) {
|
|
51
|
+
const boosts = new Map();
|
|
52
|
+
for (const r of results) {
|
|
53
|
+
const symbol = symbolTree.symbolMap.get(r.path);
|
|
54
|
+
let parent = symbol?.parent;
|
|
55
|
+
let factor = propagationFactor;
|
|
56
|
+
while (parent) {
|
|
57
|
+
const cur = boosts.get(parent.path) ?? 0;
|
|
58
|
+
boosts.set(parent.path, Math.max(cur, r.bm25Score * factor));
|
|
59
|
+
parent = parent.parent;
|
|
60
|
+
factor *= propagationFactor;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const existingPaths = new Set(results.map((r) => r.path));
|
|
64
|
+
const boosted = [];
|
|
65
|
+
for (const [parentPath, score] of boosts.entries()) {
|
|
66
|
+
if (existingPaths.has(parentPath))
|
|
67
|
+
continue;
|
|
68
|
+
const doc = getSummarySource(parentPath, summaryMap, documentMap);
|
|
69
|
+
if (!doc)
|
|
70
|
+
continue;
|
|
71
|
+
// Propagate the strongest child BM25 signal upward, then apply the parent
|
|
72
|
+
// summary's own scoring exactly once. This avoids double-counting lifecycle
|
|
73
|
+
// weights that are already baked into child compound scores.
|
|
74
|
+
const finalScore = doc.scoring
|
|
75
|
+
? compoundScore(score, doc.scoring.importance ?? 50, doc.scoring.recency ?? 0.5, doc.scoring.maturity ?? 'draft')
|
|
76
|
+
: score;
|
|
77
|
+
boosted.push({
|
|
78
|
+
backlinkCount: 0,
|
|
79
|
+
excerpt: doc.excerpt,
|
|
80
|
+
path: parentPath,
|
|
81
|
+
score: finalScore,
|
|
82
|
+
symbolKind: 'summary',
|
|
83
|
+
title: parentPath,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return boosted;
|
|
87
|
+
}
|
|
88
|
+
/** Numeric rank for maturity tiers — used for minMaturity filtering in both BM25 and propagated results. */
|
|
89
|
+
const MATURITY_TIER_RANK = { core: 3, draft: 1, validated: 2 };
|
|
36
90
|
const MINISEARCH_OPTIONS = {
|
|
37
91
|
fields: ['title', 'content', 'path'],
|
|
38
92
|
idField: 'id',
|
|
@@ -43,6 +97,28 @@ const MINISEARCH_OPTIONS = {
|
|
|
43
97
|
},
|
|
44
98
|
storeFields: ['title', 'path'],
|
|
45
99
|
};
|
|
100
|
+
function getSummaryAccessPath(path, summaryMap, documentMap) {
|
|
101
|
+
return getSummarySource(path, summaryMap, documentMap)?.path ?? `${path}/${SUMMARY_INDEX_FILE}`;
|
|
102
|
+
}
|
|
103
|
+
function getSummarySource(path, summaryMap, documentMap) {
|
|
104
|
+
const summaryDoc = summaryMap.get(`${path}/${SUMMARY_INDEX_FILE}`);
|
|
105
|
+
if (summaryDoc) {
|
|
106
|
+
return {
|
|
107
|
+
excerpt: summaryDoc.excerpt ?? '',
|
|
108
|
+
path: summaryDoc.path,
|
|
109
|
+
scoring: summaryDoc.scoring,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const contextDoc = documentMap.get(`${path}/context.md`);
|
|
113
|
+
if (contextDoc) {
|
|
114
|
+
return {
|
|
115
|
+
excerpt: extractExcerpt(contextDoc.content, contextDoc.title),
|
|
116
|
+
path: contextDoc.path,
|
|
117
|
+
scoring: contextDoc.scoring,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
46
122
|
function filterStopWords(query) {
|
|
47
123
|
const words = query.toLowerCase().split(/\s+/);
|
|
48
124
|
const filtered = removeStopwords(words);
|
|
@@ -162,6 +238,9 @@ function extractExcerpt(content, query, maxLength = 800) {
|
|
|
162
238
|
}
|
|
163
239
|
return excerpt || cleanContent.slice(0, maxLength) + (cleanContent.length > maxLength ? '...' : '');
|
|
164
240
|
}
|
|
241
|
+
function stripMarkdownFrontmatter(content) {
|
|
242
|
+
return content.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, '').trim();
|
|
243
|
+
}
|
|
165
244
|
async function findMarkdownFilesWithMtime(fileSystem, contextTreePath) {
|
|
166
245
|
try {
|
|
167
246
|
const globResult = await fileSystem.globFiles(`**/*${CONTEXT_FILE_EXTENSION}`, {
|
|
@@ -213,19 +292,27 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
|
|
|
213
292
|
symbolTree: { root: [], symbolMap: new Map() },
|
|
214
293
|
};
|
|
215
294
|
}
|
|
216
|
-
// Partition files: _index.md → summaryFiles,
|
|
295
|
+
// Partition files: _index.md → summaryFiles, .overview.md → overviewFiles (for cache
|
|
296
|
+
// invalidation + sibling detection), other derived artifacts → skip, rest → indexable
|
|
217
297
|
const summaryFiles = [];
|
|
298
|
+
const overviewFiles = [];
|
|
218
299
|
const indexableFiles = [];
|
|
300
|
+
// Track all known paths for sibling detection (e.g. .overview.md presence check)
|
|
301
|
+
const knownPaths = new Set(filesWithMtime.map((f) => f.path));
|
|
219
302
|
for (const file of filesWithMtime) {
|
|
220
303
|
const fileName = file.path.split('/').at(-1) ?? '';
|
|
221
304
|
if (fileName === SUMMARY_INDEX_FILE) {
|
|
222
305
|
summaryFiles.push(file);
|
|
223
306
|
}
|
|
307
|
+
else if (file.path.endsWith(OVERVIEW_EXTENSION)) {
|
|
308
|
+
// Track mtimes so cache invalidates when a new .overview.md appears; not BM25-indexed
|
|
309
|
+
overviewFiles.push(file);
|
|
310
|
+
}
|
|
224
311
|
else if (!isDerivedArtifact(file.path)) {
|
|
225
312
|
// Includes regular .md files AND .stub.md files (stubs are searchable)
|
|
226
313
|
indexableFiles.push(file);
|
|
227
314
|
}
|
|
228
|
-
// .full.md and _manifest.json are skipped (isDerivedArtifact returns true)
|
|
315
|
+
// .full.md, .abstract.md, and _manifest.json are skipped (isDerivedArtifact returns true)
|
|
229
316
|
}
|
|
230
317
|
// Read indexable documents for BM25 index
|
|
231
318
|
const documentPromises = indexableFiles.map(async ({ mtime, path: filePath }) => {
|
|
@@ -234,10 +321,14 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
|
|
|
234
321
|
const { content } = await fileSystem.readFile(fullPath);
|
|
235
322
|
const title = extractTitle(content, filePath.replace(/\.md$/, '').split('/').pop() || filePath);
|
|
236
323
|
const scoring = parseFrontmatterScoring(content) ?? applyDefaultScoring();
|
|
324
|
+
// Check if a .overview.md sibling exists (written by abstract generation queue)
|
|
325
|
+
const overviewRelPath = filePath.replace(/\.md$/, OVERVIEW_EXTENSION);
|
|
326
|
+
const overviewPath = knownPaths.has(overviewRelPath) ? overviewRelPath : undefined;
|
|
237
327
|
return {
|
|
238
328
|
content,
|
|
239
329
|
id: filePath,
|
|
240
330
|
mtime,
|
|
331
|
+
...(overviewPath !== undefined && { overviewPath }),
|
|
241
332
|
path: filePath,
|
|
242
333
|
scoring,
|
|
243
334
|
title,
|
|
@@ -255,9 +346,16 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
|
|
|
255
346
|
const fm = parseSummaryFrontmatter(content);
|
|
256
347
|
if (!fm)
|
|
257
348
|
return null;
|
|
349
|
+
// Persist frontmatter scoring so propagateScoresToParents can apply hotness/tier boosts
|
|
350
|
+
const frontmatter = parseFrontmatterScoring(content);
|
|
351
|
+
const scoring = frontmatter
|
|
352
|
+
? { importance: frontmatter.importance, maturity: frontmatter.maturity, recency: frontmatter.recency }
|
|
353
|
+
: undefined;
|
|
258
354
|
return {
|
|
259
355
|
condensationOrder: fm.condensation_order,
|
|
356
|
+
excerpt: stripMarkdownFrontmatter(content).slice(0, 400),
|
|
260
357
|
path: filePath,
|
|
358
|
+
scoring,
|
|
261
359
|
tokenCount: fm.token_count,
|
|
262
360
|
};
|
|
263
361
|
}
|
|
@@ -280,6 +378,10 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
|
|
|
280
378
|
for (const sf of summaryFiles) {
|
|
281
379
|
fileMtimes.set(sf.path, sf.mtime);
|
|
282
380
|
}
|
|
381
|
+
// Track .overview.md mtimes so the cache invalidates when a new overview is written
|
|
382
|
+
for (const ov of overviewFiles) {
|
|
383
|
+
fileMtimes.set(ov.path, ov.mtime);
|
|
384
|
+
}
|
|
283
385
|
const summaryMap = new Map();
|
|
284
386
|
for (const summary of summaryResults) {
|
|
285
387
|
if (summary) {
|
|
@@ -345,12 +447,27 @@ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeB
|
|
|
345
447
|
};
|
|
346
448
|
}
|
|
347
449
|
}
|
|
348
|
-
|
|
450
|
+
let allFiles = await findMarkdownFilesWithMtime(fileSystem, contextTreePath);
|
|
349
451
|
// Exclude non-indexable derived artifacts (.full.md) so that currentFiles
|
|
350
452
|
// matches what buildFreshIndex tracks in fileMtimes. Without this filter,
|
|
351
453
|
// isCacheValid() sees a size mismatch once archives exist, causing cache thrash.
|
|
352
454
|
// _index.md is kept (tracked for summary staleness), .stub.md is kept (BM25 indexed).
|
|
353
|
-
|
|
455
|
+
// Keep _index.md (summary tracking) and .overview.md (sibling detection for overviewPath).
|
|
456
|
+
// .full.md, .abstract.md, and _manifest.json remain excluded.
|
|
457
|
+
let currentFiles = allFiles.filter((f) => !isDerivedArtifact(f.path) ||
|
|
458
|
+
f.path.split('/').at(-1) === SUMMARY_INDEX_FILE ||
|
|
459
|
+
f.path.endsWith(OVERVIEW_EXTENSION));
|
|
460
|
+
// Flush pending access hits before reusing a stale-enough cache entry.
|
|
461
|
+
// The flush updates frontmatter on disk, so refresh mtimes before the cache-valid check.
|
|
462
|
+
if (onBeforeBuild) {
|
|
463
|
+
const wroteScoringUpdates = await onBeforeBuild(contextTreePath);
|
|
464
|
+
if (wroteScoringUpdates) {
|
|
465
|
+
allFiles = await findMarkdownFilesWithMtime(fileSystem, contextTreePath);
|
|
466
|
+
currentFiles = allFiles.filter((f) => !isDerivedArtifact(f.path) ||
|
|
467
|
+
f.path.split('/').at(-1) === SUMMARY_INDEX_FILE ||
|
|
468
|
+
f.path.endsWith(OVERVIEW_EXTENSION));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
354
471
|
// Re-check cache validity after getting file list (another call may have finished)
|
|
355
472
|
if (state.cachedIndex &&
|
|
356
473
|
state.cachedIndex.contextTreePath === contextTreePath &&
|
|
@@ -364,10 +481,6 @@ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeB
|
|
|
364
481
|
state.cachedIndex = updatedCache;
|
|
365
482
|
return updatedCache;
|
|
366
483
|
}
|
|
367
|
-
// Flush pending access hits before building so updated scoring is picked up
|
|
368
|
-
if (onBeforeBuild) {
|
|
369
|
-
await onBeforeBuild(contextTreePath);
|
|
370
|
-
}
|
|
371
484
|
// Build fresh index
|
|
372
485
|
const freshIndex = await buildFreshIndex(fileSystem, contextTreePath, currentFiles);
|
|
373
486
|
state.cachedIndex = freshIndex;
|
|
@@ -420,7 +533,7 @@ export class SearchKnowledgeService {
|
|
|
420
533
|
*/
|
|
421
534
|
async flushAccessHits(contextTreePath) {
|
|
422
535
|
if (this.pendingAccessHits.size === 0) {
|
|
423
|
-
return;
|
|
536
|
+
return false;
|
|
424
537
|
}
|
|
425
538
|
const hits = new Map(this.pendingAccessHits);
|
|
426
539
|
this.pendingAccessHits.clear();
|
|
@@ -440,6 +553,7 @@ export class SearchKnowledgeService {
|
|
|
440
553
|
}
|
|
441
554
|
});
|
|
442
555
|
await Promise.allSettled(tasks);
|
|
556
|
+
return true;
|
|
443
557
|
}
|
|
444
558
|
/**
|
|
445
559
|
* Search the knowledge base for relevant topics.
|
|
@@ -451,14 +565,15 @@ export class SearchKnowledgeService {
|
|
|
451
565
|
*/
|
|
452
566
|
async search(query, options) {
|
|
453
567
|
const limit = options?.limit ?? 10;
|
|
454
|
-
const
|
|
568
|
+
const resolvedBaseDirectory = await realpath(this.baseDirectory).catch(() => this.baseDirectory);
|
|
569
|
+
const contextTreePath = join(resolvedBaseDirectory, BRV_DIR, CONTEXT_TREE_DIR);
|
|
455
570
|
// Acquire index with parallel-safe locking; flush pending access hits before any rebuild
|
|
456
571
|
const indexResult = await acquireIndex(this.state, this.fileSystem, contextTreePath, this.cacheTtlMs, (ctxPath) => this.flushAccessHits(ctxPath));
|
|
457
572
|
// Handle error case (context tree not initialized)
|
|
458
573
|
if ('error' in indexResult) {
|
|
459
574
|
return indexResult.result;
|
|
460
575
|
}
|
|
461
|
-
const { documentMap, index, referenceIndex, symbolTree } = indexResult;
|
|
576
|
+
const { documentMap, index, referenceIndex, summaryMap, symbolTree } = indexResult;
|
|
462
577
|
if (documentMap.size === 0) {
|
|
463
578
|
return {
|
|
464
579
|
message: 'Context tree is empty. Use /curate to add knowledge.',
|
|
@@ -472,7 +587,7 @@ export class SearchKnowledgeService {
|
|
|
472
587
|
}
|
|
473
588
|
// Symbolic path resolution: try path-based query first
|
|
474
589
|
if (isPathLikeQuery(query, symbolTree)) {
|
|
475
|
-
const symbolicResult = this.trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, options);
|
|
590
|
+
const symbolicResult = this.trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, summaryMap, options);
|
|
476
591
|
if (symbolicResult) {
|
|
477
592
|
return symbolicResult;
|
|
478
593
|
}
|
|
@@ -482,10 +597,10 @@ export class SearchKnowledgeService {
|
|
|
482
597
|
const effectiveScope = options?.scope ?? parsed.scopePath;
|
|
483
598
|
const effectiveQuery = parsed.scopePath ? parsed.textQuery : query;
|
|
484
599
|
// Run text-based MiniSearch (existing pipeline), optionally scoped to a subtree
|
|
485
|
-
const textResult = this.runTextSearch(effectiveQuery || query, documentMap, index, limit, effectiveScope, symbolTree, referenceIndex, options);
|
|
600
|
+
const textResult = this.runTextSearch(effectiveQuery || query, documentMap, index, limit, effectiveScope, symbolTree, referenceIndex, summaryMap, options);
|
|
486
601
|
// If scoped search returned nothing and we had a scope, fall back to global search
|
|
487
602
|
if (textResult.results.length === 0 && effectiveScope && effectiveQuery) {
|
|
488
|
-
return this.runTextSearch(query, documentMap, index, limit, undefined, symbolTree, referenceIndex, options);
|
|
603
|
+
return this.runTextSearch(query, documentMap, index, limit, undefined, symbolTree, referenceIndex, summaryMap, options);
|
|
489
604
|
}
|
|
490
605
|
return textResult;
|
|
491
606
|
}
|
|
@@ -536,19 +651,27 @@ export class SearchKnowledgeService {
|
|
|
536
651
|
}
|
|
537
652
|
}
|
|
538
653
|
}
|
|
654
|
+
const doc = documentMap.get(result.path);
|
|
655
|
+
const overviewPath = doc?.overviewPath;
|
|
656
|
+
const isContextSummary = doc?.path.endsWith('/context.md') || doc?.path === 'context.md';
|
|
657
|
+
const summaryPath = isContextSummary
|
|
658
|
+
? doc?.path.slice(0, -'/context.md'.length) || doc?.path || result.path
|
|
659
|
+
: result.path;
|
|
539
660
|
return {
|
|
540
661
|
...result,
|
|
541
662
|
...(archiveFullPath && { archiveFullPath }),
|
|
663
|
+
...(overviewPath && { overviewPath }),
|
|
542
664
|
backlinkCount: backlinks?.length ?? 0,
|
|
665
|
+
...(isContextSummary && { path: summaryPath }),
|
|
543
666
|
relatedPaths: backlinks?.slice(0, 3),
|
|
544
|
-
symbolKind,
|
|
545
|
-
symbolPath: symbol?.path,
|
|
667
|
+
symbolKind: isContextSummary ? 'summary' : symbolKind,
|
|
668
|
+
symbolPath: isContextSummary ? summaryPath : symbol?.path,
|
|
546
669
|
};
|
|
547
670
|
}
|
|
548
671
|
/**
|
|
549
672
|
* Run the standard text-based MiniSearch pipeline, optionally scoped to a subtree.
|
|
550
673
|
*/
|
|
551
|
-
runTextSearch(query, documentMap, index, limit, scopePath, symbolTree, referenceIndex, options) {
|
|
674
|
+
runTextSearch(query, documentMap, index, limit, scopePath, symbolTree, referenceIndex, summaryMap, options) {
|
|
552
675
|
const filteredQuery = filterStopWords(query);
|
|
553
676
|
const filteredWords = filteredQuery.split(/\s+/).filter((w) => w.length >= 2);
|
|
554
677
|
// Build scope filter if a subtree is specified
|
|
@@ -585,11 +708,14 @@ export class SearchKnowledgeService {
|
|
|
585
708
|
const bm25 = normalizeScore(r.score);
|
|
586
709
|
return {
|
|
587
710
|
...r,
|
|
711
|
+
bm25Score: bm25,
|
|
588
712
|
score: compoundScore(bm25, decayed.importance ?? 50, decayed.recency ?? 1, decayed.maturity ?? 'draft'),
|
|
589
713
|
};
|
|
590
714
|
});
|
|
591
715
|
searchResults.sort((a, b) => b.score - a.score);
|
|
592
716
|
const results = [];
|
|
717
|
+
const propagationInputs = [];
|
|
718
|
+
let scoreFloor;
|
|
593
719
|
if (searchResults.length > 0) {
|
|
594
720
|
// OOD detection: if the best result scores below the minimum floor,
|
|
595
721
|
// the query has no meaningful match in the knowledge base.
|
|
@@ -615,7 +741,7 @@ export class SearchKnowledgeService {
|
|
|
615
741
|
};
|
|
616
742
|
}
|
|
617
743
|
const topScore = searchResults[0].score;
|
|
618
|
-
|
|
744
|
+
scoreFloor = topScore * SCORE_GAP_RATIO;
|
|
619
745
|
const resultLimit = Math.min(limit, searchResults.length);
|
|
620
746
|
for (let i = 0; i < resultLimit; i++) {
|
|
621
747
|
const result = searchResults[i];
|
|
@@ -638,22 +764,54 @@ export class SearchKnowledgeService {
|
|
|
638
764
|
continue;
|
|
639
765
|
}
|
|
640
766
|
if (options?.minMaturity && enriched.symbolKind) {
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
767
|
+
const docMaturity = enriched.symbolKind === 'summary'
|
|
768
|
+
? getSummarySource(enriched.path, summaryMap, documentMap)?.scoring?.maturity
|
|
769
|
+
?? symbolTree.symbolMap.get(enriched.path)?.metadata.maturity
|
|
770
|
+
?? 'draft'
|
|
771
|
+
: symbolTree.symbolMap.get(document.path)?.metadata.maturity ?? 'draft';
|
|
772
|
+
if ((MATURITY_TIER_RANK[docMaturity] ?? 1) < (MATURITY_TIER_RANK[options.minMaturity] ?? 1)) {
|
|
645
773
|
continue;
|
|
646
774
|
}
|
|
647
775
|
}
|
|
648
776
|
results.push(enriched);
|
|
777
|
+
propagationInputs.push({
|
|
778
|
+
bm25Score: result.bm25Score,
|
|
779
|
+
path: document.path,
|
|
780
|
+
});
|
|
649
781
|
}
|
|
650
782
|
}
|
|
651
783
|
}
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
784
|
+
// Propagate scores upward to parent domain/topic nodes (hierarchical retrieval)
|
|
785
|
+
const propagated = propagateScoresToParents(propagationInputs, symbolTree, summaryMap, documentMap);
|
|
786
|
+
for (const p of propagated) {
|
|
787
|
+
if (scoreFloor !== undefined && p.score < scoreFloor)
|
|
788
|
+
continue;
|
|
789
|
+
if (options?.includeKinds && p.symbolKind && !options.includeKinds.includes(p.symbolKind))
|
|
790
|
+
continue;
|
|
791
|
+
if (options?.excludeKinds && p.symbolKind && options.excludeKinds.includes(p.symbolKind))
|
|
792
|
+
continue;
|
|
793
|
+
if (options?.minMaturity && p.symbolKind === 'summary') {
|
|
794
|
+
const summaryDoc = getSummarySource(p.path, summaryMap, documentMap);
|
|
795
|
+
const summaryMaturity = summaryDoc?.scoring?.maturity ?? 'draft';
|
|
796
|
+
if ((MATURITY_TIER_RANK[summaryMaturity] ?? 1) < (MATURITY_TIER_RANK[options.minMaturity] ?? 1))
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
results.push(p);
|
|
800
|
+
}
|
|
801
|
+
if (propagated.length > 0) {
|
|
802
|
+
results.sort((a, b) => b.score - a.score);
|
|
803
|
+
// Trim back to the caller-requested limit after propagated entries are merged in.
|
|
804
|
+
if (results.length > limit)
|
|
805
|
+
results.splice(limit);
|
|
806
|
+
}
|
|
807
|
+
// Accumulate access hits for returned results (flushed during next index rebuild).
|
|
808
|
+
// Synthetic 'summary' results carry folder-style paths (e.g. 'auth') that are not
|
|
809
|
+
// real files; map them to their _index.md so flushAccessHits can read and update them.
|
|
810
|
+
if (results.length > 0) {
|
|
811
|
+
this.accumulateAccessHits(results.map((r) => (r.symbolKind === 'summary'
|
|
812
|
+
? getSummaryAccessPath(r.path, summaryMap, documentMap)
|
|
813
|
+
: r.path)));
|
|
814
|
+
}
|
|
657
815
|
return {
|
|
658
816
|
message: results.length > 0
|
|
659
817
|
? `Found ${searchResults.length} result(s). Use read_file to view full content.`
|
|
@@ -665,7 +823,7 @@ export class SearchKnowledgeService {
|
|
|
665
823
|
/**
|
|
666
824
|
* Try to resolve the query as a symbolic path. Returns null if no path match found.
|
|
667
825
|
*/
|
|
668
|
-
trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, options) {
|
|
826
|
+
trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, summaryMap, options) {
|
|
669
827
|
const pathMatches = matchMemoryPath(symbolTree, query.split(/\s+/)[0].includes('/') ? query.split(/\s+/)[0] : query);
|
|
670
828
|
if (pathMatches.length === 0) {
|
|
671
829
|
return null;
|
|
@@ -691,11 +849,25 @@ export class SearchKnowledgeService {
|
|
|
691
849
|
const textPart = query.slice(query.indexOf(pathPart) + pathPart.length).trim();
|
|
692
850
|
if (textPart) {
|
|
693
851
|
// Scoped search: search text within the matched subtree
|
|
694
|
-
return this.runTextSearch(textPart, documentMap, index, limit, topMatch.path, symbolTree, referenceIndex, options);
|
|
852
|
+
return this.runTextSearch(textPart, documentMap, index, limit, topMatch.path, symbolTree, referenceIndex, summaryMap, options);
|
|
695
853
|
}
|
|
696
854
|
// No text part — return all children of the matched node
|
|
697
855
|
const subtreeIds = getSubtreeDocumentIds(symbolTree, topMatch.path);
|
|
698
856
|
const results = [];
|
|
857
|
+
const accessHitPaths = [];
|
|
858
|
+
const summaryDoc = getSummarySource(topMatch.path, summaryMap, documentMap);
|
|
859
|
+
if (summaryDoc) {
|
|
860
|
+
results.push({
|
|
861
|
+
backlinkCount: 0,
|
|
862
|
+
excerpt: summaryDoc.excerpt,
|
|
863
|
+
path: topMatch.path,
|
|
864
|
+
score: 1,
|
|
865
|
+
symbolKind: 'summary',
|
|
866
|
+
symbolPath: topMatch.path,
|
|
867
|
+
title: topMatch.name,
|
|
868
|
+
});
|
|
869
|
+
accessHitPaths.push(summaryDoc.path);
|
|
870
|
+
}
|
|
699
871
|
for (const docId of subtreeIds) {
|
|
700
872
|
if (results.length >= limit)
|
|
701
873
|
break;
|
|
@@ -703,9 +875,10 @@ export class SearchKnowledgeService {
|
|
|
703
875
|
if (!doc)
|
|
704
876
|
continue;
|
|
705
877
|
results.push(this.enrichResult({ excerpt: extractExcerpt(doc.content, query), path: doc.path, score: 0.9, title: doc.title }, symbolTree, referenceIndex, documentMap));
|
|
878
|
+
accessHitPaths.push(doc.path);
|
|
706
879
|
}
|
|
707
|
-
if (
|
|
708
|
-
this.accumulateAccessHits(
|
|
880
|
+
if (accessHitPaths.length > 0) {
|
|
881
|
+
this.accumulateAccessHits(accessHitPaths);
|
|
709
882
|
}
|
|
710
883
|
return {
|
|
711
884
|
message: `Found ${results.length} entries under ${topMatch.path}. Use read_file to view full content.`,
|
|
@@ -4,11 +4,11 @@ import { SearchKnowledgeService } from './search-knowledge-service.js';
|
|
|
4
4
|
const SearchKnowledgeInputSchema = z
|
|
5
5
|
.object({
|
|
6
6
|
excludeKinds: z
|
|
7
|
-
.array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'topic']))
|
|
7
|
+
.array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'summary', 'topic']))
|
|
8
8
|
.optional()
|
|
9
9
|
.describe('Symbol kinds to exclude from results'),
|
|
10
10
|
includeKinds: z
|
|
11
|
-
.array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'topic']))
|
|
11
|
+
.array(z.enum(['archive_stub', 'context', 'domain', 'subtopic', 'summary', 'topic']))
|
|
12
12
|
.optional()
|
|
13
13
|
.describe('Symbol kinds to include in results (filters out others)'),
|
|
14
14
|
limit: z
|
|
@@ -9,6 +9,7 @@ import type { IProcessService } from '../../core/interfaces/i-process-service.js
|
|
|
9
9
|
import type { ISandboxService } from '../../core/interfaces/i-sandbox-service.js';
|
|
10
10
|
import type { ITodoStorage } from '../../core/interfaces/i-todo-storage.js';
|
|
11
11
|
import type { ITokenizer } from '../../core/interfaces/i-tokenizer.js';
|
|
12
|
+
import type { AbstractGenerationQueue } from '../map/abstract-queue.js';
|
|
12
13
|
import type { MemoryManager } from '../memory/memory-manager.js';
|
|
13
14
|
import type { ToolProviderGetter } from './tool-provider-getter.js';
|
|
14
15
|
import { ToolMarker } from './tool-markers.js';
|
|
@@ -17,6 +18,8 @@ import { ToolMarker } from './tool-markers.js';
|
|
|
17
18
|
* Tools declare which services they need via requiredServices.
|
|
18
19
|
*/
|
|
19
20
|
export interface ToolServices {
|
|
21
|
+
/** Abstract generation queue for background L0/L1 abstract file generation */
|
|
22
|
+
abstractQueue?: AbstractGenerationQueue;
|
|
20
23
|
/** Agent instance for creating sub-sessions (used by agentic_map) */
|
|
21
24
|
agentInstance?: ICipherAgent;
|
|
22
25
|
/** Content generator for stateless LLM calls (used by llm_map) */
|
|
@@ -6,6 +6,7 @@ import { createCurateTool } from './implementations/curate-tool.js';
|
|
|
6
6
|
import { createExpandKnowledgeTool } from './implementations/expand-knowledge-tool.js';
|
|
7
7
|
import { createGlobFilesTool } from './implementations/glob-files-tool.js';
|
|
8
8
|
import { createGrepContentTool } from './implementations/grep-content-tool.js';
|
|
9
|
+
import { createIngestResourceTool } from './implementations/ingest-resource-tool.js';
|
|
9
10
|
import { createListDirectoryTool } from './implementations/list-directory-tool.js';
|
|
10
11
|
import { createLlmMapTool } from './implementations/llm-map-tool.js';
|
|
11
12
|
import { createReadFileTool } from './implementations/read-file-tool.js';
|
|
@@ -54,7 +55,7 @@ export const TOOL_REGISTRY = {
|
|
|
54
55
|
},
|
|
55
56
|
[ToolName.CODE_EXEC]: {
|
|
56
57
|
descriptionFile: 'code_exec',
|
|
57
|
-
factory({ environmentContext, fileSystemService, sandboxService }) {
|
|
58
|
+
factory({ abstractQueue, environmentContext, fileSystemService, sandboxService }) {
|
|
58
59
|
const sandbox = getRequiredService(sandboxService, 'sandboxService');
|
|
59
60
|
// Inject file system service into sandbox for Tools SDK
|
|
60
61
|
if (fileSystemService && sandbox.setFileSystem) {
|
|
@@ -67,7 +68,7 @@ export const TOOL_REGISTRY = {
|
|
|
67
68
|
}
|
|
68
69
|
// Inject curate service into sandbox for Tools SDK
|
|
69
70
|
if (sandbox.setCurateService) {
|
|
70
|
-
const curateService = createCurateService(environmentContext?.workingDirectory);
|
|
71
|
+
const curateService = createCurateService(environmentContext?.workingDirectory, abstractQueue);
|
|
71
72
|
sandbox.setCurateService(curateService);
|
|
72
73
|
}
|
|
73
74
|
// Inject environment context into sandbox for env.* access
|
|
@@ -81,10 +82,10 @@ export const TOOL_REGISTRY = {
|
|
|
81
82
|
},
|
|
82
83
|
[ToolName.CURATE]: {
|
|
83
84
|
descriptionFile: 'curate',
|
|
84
|
-
factory: ({ environmentContext }) => createCurateTool(environmentContext?.workingDirectory),
|
|
85
|
+
factory: ({ abstractQueue, environmentContext }) => createCurateTool(environmentContext?.workingDirectory, abstractQueue),
|
|
85
86
|
markers: [ToolMarker.ContextBuilding, ToolMarker.Modification],
|
|
86
87
|
outputGuidance: 'curate',
|
|
87
|
-
requiredServices: [],
|
|
88
|
+
requiredServices: [],
|
|
88
89
|
},
|
|
89
90
|
[ToolName.EXPAND_KNOWLEDGE]: {
|
|
90
91
|
descriptionFile: 'expand_knowledge',
|
|
@@ -104,6 +105,16 @@ export const TOOL_REGISTRY = {
|
|
|
104
105
|
markers: [ToolMarker.Core, ToolMarker.Discovery],
|
|
105
106
|
requiredServices: ['fileSystemService'],
|
|
106
107
|
},
|
|
108
|
+
[ToolName.INGEST_RESOURCE]: {
|
|
109
|
+
factory: ({ abstractQueue, contentGenerator, environmentContext, fileSystemService }) => createIngestResourceTool({
|
|
110
|
+
abstractQueue,
|
|
111
|
+
baseDirectory: environmentContext?.workingDirectory,
|
|
112
|
+
contentGenerator,
|
|
113
|
+
fileSystem: fileSystemService,
|
|
114
|
+
}),
|
|
115
|
+
markers: [ToolMarker.ContextBuilding, ToolMarker.Modification],
|
|
116
|
+
requiredServices: ['contentGenerator', 'fileSystemService'],
|
|
117
|
+
},
|
|
107
118
|
[ToolName.LIST_DIRECTORY]: {
|
|
108
119
|
descriptionFile: 'list_directory',
|
|
109
120
|
factory: (services) => createListDirectoryTool(getRequiredService(services.fileSystemService, 'fileSystemService')),
|
|
@@ -55,6 +55,8 @@ export declare const SUMMARY_INDEX_FILE = "_index.md";
|
|
|
55
55
|
export declare const ARCHIVE_DIR = "_archived";
|
|
56
56
|
export declare const STUB_EXTENSION = ".stub.md";
|
|
57
57
|
export declare const FULL_ARCHIVE_EXTENSION = ".full.md";
|
|
58
|
+
export declare const ABSTRACT_EXTENSION = ".abstract.md";
|
|
59
|
+
export declare const OVERVIEW_EXTENSION = ".overview.md";
|
|
58
60
|
export declare const MANIFEST_FILE = "_manifest.json";
|
|
59
61
|
export declare const ARCHIVE_IMPORTANCE_THRESHOLD = 35;
|
|
60
62
|
export declare const DEFAULT_GHOST_CUE_MAX_TOKENS = 220;
|
package/dist/server/constants.js
CHANGED
|
@@ -75,6 +75,8 @@ export const SUMMARY_INDEX_FILE = '_index.md';
|
|
|
75
75
|
export const ARCHIVE_DIR = '_archived';
|
|
76
76
|
export const STUB_EXTENSION = '.stub.md';
|
|
77
77
|
export const FULL_ARCHIVE_EXTENSION = '.full.md';
|
|
78
|
+
export const ABSTRACT_EXTENSION = '.abstract.md';
|
|
79
|
+
export const OVERVIEW_EXTENSION = '.overview.md';
|
|
78
80
|
export const MANIFEST_FILE = '_manifest.json';
|
|
79
81
|
export const ARCHIVE_IMPORTANCE_THRESHOLD = 35;
|
|
80
82
|
export const DEFAULT_GHOST_CUE_MAX_TOKENS = 220;
|
|
@@ -19,11 +19,11 @@ export declare const ACCESS_IMPORTANCE_BONUS = 3;
|
|
|
19
19
|
/** Importance bonus per curate update */
|
|
20
20
|
export declare const UPDATE_IMPORTANCE_BONUS = 5;
|
|
21
21
|
/** BM25 relevance weight in compound score */
|
|
22
|
-
export declare const W_RELEVANCE =
|
|
22
|
+
export declare const W_RELEVANCE = 0.6;
|
|
23
23
|
/** Importance weight in compound score */
|
|
24
|
-
export declare const W_IMPORTANCE = 0;
|
|
24
|
+
export declare const W_IMPORTANCE = 0.2;
|
|
25
25
|
/** Recency weight in compound score */
|
|
26
|
-
export declare const W_RECENCY = 0;
|
|
26
|
+
export declare const W_RECENCY = 0.2;
|
|
27
27
|
/** Importance threshold to promote draft -> validated */
|
|
28
28
|
export declare const PROMOTE_TO_VALIDATED = 65;
|
|
29
29
|
/** Importance threshold to promote validated -> core */
|
|
@@ -21,11 +21,11 @@ export const ACCESS_IMPORTANCE_BONUS = 3;
|
|
|
21
21
|
/** Importance bonus per curate update */
|
|
22
22
|
export const UPDATE_IMPORTANCE_BONUS = 5;
|
|
23
23
|
/** BM25 relevance weight in compound score */
|
|
24
|
-
export const W_RELEVANCE =
|
|
24
|
+
export const W_RELEVANCE = 0.6;
|
|
25
25
|
/** Importance weight in compound score */
|
|
26
|
-
export const W_IMPORTANCE = 0;
|
|
26
|
+
export const W_IMPORTANCE = 0.2;
|
|
27
27
|
/** Recency weight in compound score */
|
|
28
|
-
export const W_RECENCY = 0;
|
|
28
|
+
export const W_RECENCY = 0.2;
|
|
29
29
|
/** Importance threshold to promote draft -> validated */
|
|
30
30
|
export const PROMOTE_TO_VALIDATED = 65;
|
|
31
31
|
/** Importance threshold to promote validated -> core */
|
|
@@ -36,8 +36,8 @@ export const DEMOTE_FROM_CORE = 60;
|
|
|
36
36
|
export const DEMOTE_FROM_VALIDATED = 35;
|
|
37
37
|
/** Search score multiplier per maturity tier */
|
|
38
38
|
export const TIER_BOOST = {
|
|
39
|
-
core: 1,
|
|
40
|
-
draft:
|
|
39
|
+
core: 1.15,
|
|
40
|
+
draft: 0.85,
|
|
41
41
|
validated: 1,
|
|
42
42
|
};
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
@@ -41,6 +41,10 @@ export interface ArchiveStubFrontmatter {
|
|
|
41
41
|
type: 'archive_stub';
|
|
42
42
|
}
|
|
43
43
|
export interface ManifestEntry {
|
|
44
|
+
/** Relative path to .abstract.md sibling, if it exists */
|
|
45
|
+
abstractPath?: string;
|
|
46
|
+
/** Token count of .abstract.md (used for lane budgeting) */
|
|
47
|
+
abstractTokens?: number;
|
|
44
48
|
/** Importance score from frontmatter (0-100, default 50) */
|
|
45
49
|
importance?: number;
|
|
46
50
|
/** Condensation order (only for summaries) */
|