sift-light 1.0.0 → 1.0.1
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/CHANGELOG.md +19 -0
- package/README.md +8 -1
- package/README.zh-CN.md +8 -1
- package/package.json +1 -1
- package/plugins/sift-light/.claude-plugin/plugin.json +1 -1
- package/plugins/sift-light/.codex-plugin/plugin.json +1 -1
- package/plugins/sift-light/.mcp.json +1 -1
- package/plugins/sift-light/concept-worker.mjs +39 -12
- package/plugins/sift-light/kimi.plugin.json +2 -2
- package/plugins/sift-light/omp-extension.mjs +68 -28
- package/plugins/sift-light/package.json +1 -1
- package/src/concept-inference.ts +44 -11
- package/src/concept-model.ts +2 -2
- package/src/concept-search.ts +12 -2
- package/src/concept-worker.mjs +39 -12
- package/src/concept-worker.ts +1 -1
- package/src/config-reader.ts +11 -3
- package/src/hybrid-search.ts +13 -0
- package/src/index.ts +4 -10
- package/src/mcp-model-output.ts +41 -0
- package/src/mcp-semantic-judge.ts +19 -8
- package/src/mcp-server.mjs +262 -191
- package/src/mcp-server.ts +11 -13
- package/src/mcp.ts +2 -0
- package/src/model-error.ts +6 -16
- package/src/omp-index.ts +4 -6
- package/src/prompt-guidelines.ts +5 -5
- package/src/request-contract.ts +41 -5
- package/src/semantic-judge.ts +17 -3
- package/src/service.ts +8 -0
- package/src/tool-schema.ts +3 -3
package/src/concept-inference.ts
CHANGED
|
@@ -21,7 +21,9 @@ export interface ConceptEmbeddingCallbacks {
|
|
|
21
21
|
) => void | Promise<void>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Bound each ONNX call to four model windows. Larger native batches raised peak
|
|
25
|
+
// RSS without a useful cold-start gain on the measured workload.
|
|
26
|
+
const INFERENCE_BATCH_SIZE = 4;
|
|
25
27
|
|
|
26
28
|
/** Snap a candidate UTF-16 index so it never splits a surrogate pair. */
|
|
27
29
|
export function safeUtf16End(text: string, end: number): number {
|
|
@@ -62,6 +64,10 @@ export function maximumTokenSafeEnd(
|
|
|
62
64
|
text: string,
|
|
63
65
|
start: number,
|
|
64
66
|
): number {
|
|
67
|
+
// Most source passages already fit the model window. Avoid repeatedly
|
|
68
|
+
// tokenizing prefixes of the same passage in the binary-search path.
|
|
69
|
+
if (tokenCount(extractor, `${prefix}${text.slice(start)}`) <= CONCEPT_MODEL_TOKENS)
|
|
70
|
+
return text.length;
|
|
65
71
|
let low = start + 1;
|
|
66
72
|
let high = text.length;
|
|
67
73
|
let accepted = start;
|
|
@@ -142,30 +148,56 @@ export async function embedConceptInputs(
|
|
|
142
148
|
): Promise<CachedConceptEmbedding[]> {
|
|
143
149
|
const layouts = pending.map((item) => tokenSafeWindows(extractor, item));
|
|
144
150
|
const inputs = layouts.flatMap((windows) => windows.map((window) => window.input));
|
|
145
|
-
const vectors: number[][] =
|
|
151
|
+
const vectors: (number[] | undefined)[] = Array(inputs.length);
|
|
146
152
|
const layoutEnds: number[] = [];
|
|
153
|
+
const remainingWindows = layouts.map((windows) => windows.length);
|
|
147
154
|
let layoutEnd = 0;
|
|
148
155
|
for (const windows of layouts) {
|
|
149
156
|
layoutEnd += windows.length;
|
|
150
157
|
layoutEnds.push(layoutEnd);
|
|
151
158
|
}
|
|
159
|
+
const orderedInputs = inputs
|
|
160
|
+
.map((input, index) => ({ input, index, tokens: tokenCount(extractor, input) }))
|
|
161
|
+
// Start with the largest shapes so the native arena can reuse allocations
|
|
162
|
+
// for smaller batches instead of retaining a growing sequence of arenas.
|
|
163
|
+
.toSorted((left, right) => right.tokens - left.tokens || left.index - right.index);
|
|
164
|
+
const vectorFor = (index: number): number[] => {
|
|
165
|
+
const vector = vectors[index];
|
|
166
|
+
if (!vector) throw new Error("Missing concept embedding vector");
|
|
167
|
+
return vector;
|
|
168
|
+
};
|
|
169
|
+
const ownerOf = (index: number): number => {
|
|
170
|
+
let low = 0;
|
|
171
|
+
let high = layoutEnds.length;
|
|
172
|
+
while (low < high) {
|
|
173
|
+
const middle = Math.floor((low + high) / 2);
|
|
174
|
+
if (index < layoutEnds[middle]!) high = middle;
|
|
175
|
+
else low = middle + 1;
|
|
176
|
+
}
|
|
177
|
+
return low;
|
|
178
|
+
};
|
|
152
179
|
let completedEmbedding = 0;
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
let completedWindows = 0;
|
|
181
|
+
for (let offset = 0; offset < orderedInputs.length;) {
|
|
182
|
+
const orderedBatch = orderedInputs.slice(offset, offset + INFERENCE_BATCH_SIZE);
|
|
183
|
+
const batch = orderedBatch.map((item) => item.input);
|
|
155
184
|
// oxlint-disable-next-line no-await-in-loop -- bounded batches cap native tensor memory.
|
|
156
185
|
const tensor = await extractor(batch, { pooling: "mean", normalize: true });
|
|
157
186
|
if (tensor.data.length !== batch.length * CONCEPT_EMBEDDING_DIMENSIONS)
|
|
158
187
|
throw new Error("Unexpected concept embedding dimensions");
|
|
159
188
|
for (let index = 0; index < batch.length; index += 1) {
|
|
160
189
|
const start = index * CONCEPT_EMBEDDING_DIMENSIONS;
|
|
161
|
-
|
|
162
|
-
|
|
190
|
+
const inputIndex = orderedBatch[index]!.index;
|
|
191
|
+
vectors[inputIndex] = Array.from(
|
|
192
|
+
tensor.data.slice(start, start + CONCEPT_EMBEDDING_DIMENSIONS),
|
|
193
|
+
Number,
|
|
163
194
|
);
|
|
195
|
+
remainingWindows[ownerOf(inputIndex)]!--;
|
|
164
196
|
}
|
|
165
|
-
|
|
197
|
+
completedWindows += batch.length;
|
|
166
198
|
while (
|
|
167
|
-
|
|
168
|
-
|
|
199
|
+
remainingWindows[completedEmbedding] !== undefined &&
|
|
200
|
+
remainingWindows[completedEmbedding] === 0
|
|
169
201
|
) {
|
|
170
202
|
const start = completedEmbedding === 0 ? 0 : layoutEnds[completedEmbedding - 1]!;
|
|
171
203
|
const item = pending[completedEmbedding];
|
|
@@ -177,7 +209,7 @@ export async function embedConceptInputs(
|
|
|
177
209
|
windows: (layouts[completedEmbedding] ?? []).map((window, index) => ({
|
|
178
210
|
start: window.start,
|
|
179
211
|
end: window.end,
|
|
180
|
-
vector:
|
|
212
|
+
vector: vectorFor(start + index),
|
|
181
213
|
})),
|
|
182
214
|
},
|
|
183
215
|
completedEmbedding + 1,
|
|
@@ -186,6 +218,7 @@ export async function embedConceptInputs(
|
|
|
186
218
|
completedEmbedding += 1;
|
|
187
219
|
}
|
|
188
220
|
callbacks.onBatch?.(completedWindows, inputs.length);
|
|
221
|
+
offset += batch.length;
|
|
189
222
|
}
|
|
190
223
|
let vectorIndex = 0;
|
|
191
224
|
return pending.map((item, index) => ({
|
|
@@ -193,7 +226,7 @@ export async function embedConceptInputs(
|
|
|
193
226
|
windows: (layouts[index] ?? []).map((window) => ({
|
|
194
227
|
start: window.start,
|
|
195
228
|
end: window.end,
|
|
196
|
-
vector:
|
|
229
|
+
vector: vectorFor(vectorIndex++),
|
|
197
230
|
})),
|
|
198
231
|
}));
|
|
199
232
|
}
|
package/src/concept-model.ts
CHANGED
|
@@ -8,10 +8,10 @@ export const CONCEPT_MODEL = "Xenova/multilingual-e5-small";
|
|
|
8
8
|
export const CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
|
|
9
9
|
export const MAX_CONCEPT_CHARS = 1_000;
|
|
10
10
|
export const CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
|
|
11
|
-
export const CONCEPT_MODEL_TOKENS =
|
|
11
|
+
export const CONCEPT_MODEL_TOKENS = 256;
|
|
12
12
|
export const CONCEPT_WINDOW_OVERLAP_TOKENS = 64;
|
|
13
13
|
export const CONCEPT_EMBEDDING_DIMENSIONS = 384;
|
|
14
|
-
export const CONCEPT_CACHE_VERSION =
|
|
14
|
+
export const CONCEPT_CACHE_VERSION = 2;
|
|
15
15
|
export const CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
16
16
|
export const CONCEPT_TIMEOUT_MS = 10 * 60_000;
|
|
17
17
|
export const MIN_CONCEPT_TIMEOUT_MS = 1_000;
|
package/src/concept-search.ts
CHANGED
|
@@ -86,6 +86,14 @@ function scoreProfile(scores: readonly number[]): ConceptScoreProfile {
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** E5 cosine scores favor very short, generic passages in close races. Keep the
|
|
90
|
+
* raw cosine as evidence and apply a bounded length correction only to rank. */
|
|
91
|
+
export function conceptRankingScore(cosine: number, passageLength: number): number {
|
|
92
|
+
const referenceLength = 500;
|
|
93
|
+
const maximumCorrection = 0.02;
|
|
94
|
+
return cosine - maximumCorrection * (1 - Math.sqrt(Math.min(passageLength / referenceLength, 1)));
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
function passage(document: SourceDocument, start: number): { value: Passage; next: number } {
|
|
90
98
|
let end = Math.min(document.text.length, start + MAX_CONCEPT_CHARS);
|
|
91
99
|
if (end < document.text.length) {
|
|
@@ -433,6 +441,7 @@ async function runConceptSearch(
|
|
|
433
441
|
const batchItems = passages.map((item, index) => {
|
|
434
442
|
const similarity = inferred.scores[index];
|
|
435
443
|
if (similarity === undefined) throw new Error("Missing concept similarity");
|
|
444
|
+
const rankingScore = conceptRankingScore(similarity, item.text.length);
|
|
436
445
|
const evidence = rangeEvidence(item.document, item.range);
|
|
437
446
|
return {
|
|
438
447
|
path: item.document.path,
|
|
@@ -445,8 +454,9 @@ async function runConceptSearch(
|
|
|
445
454
|
kind: "concept-candidate",
|
|
446
455
|
certainty: "candidate",
|
|
447
456
|
score: similarity,
|
|
457
|
+
rankingScore,
|
|
448
458
|
rankingReason:
|
|
449
|
-
"local multilingual E5 cosine similarity; relevance candidate, no binding or execution claim",
|
|
459
|
+
"local multilingual E5 cosine similarity with bounded short-passage rank correction; relevance candidate, no binding or execution claim",
|
|
450
460
|
model: CONCEPT_MODEL,
|
|
451
461
|
revision: CONCEPT_REVISION,
|
|
452
462
|
tokenTruncated: false,
|
|
@@ -457,7 +467,7 @@ async function runConceptSearch(
|
|
|
457
467
|
});
|
|
458
468
|
const ranked = [...result.items, ...batchItems].toSorted(
|
|
459
469
|
(a, b) =>
|
|
460
|
-
Number(b.details?.
|
|
470
|
+
Number(b.details?.rankingScore) - Number(a.details?.rankingScore) ||
|
|
461
471
|
a.path.localeCompare(b.path) ||
|
|
462
472
|
a.line - b.line,
|
|
463
473
|
);
|
package/src/concept-worker.mjs
CHANGED
|
@@ -27,10 +27,10 @@ class SiftLightError extends Error {
|
|
|
27
27
|
var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
|
|
28
28
|
var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
|
|
29
29
|
var MAX_CONCEPT_CHARS = 1000;
|
|
30
|
-
var CONCEPT_MODEL_TOKENS =
|
|
30
|
+
var CONCEPT_MODEL_TOKENS = 256;
|
|
31
31
|
var CONCEPT_WINDOW_OVERLAP_TOKENS = 64;
|
|
32
32
|
var CONCEPT_EMBEDDING_DIMENSIONS = 384;
|
|
33
|
-
var CONCEPT_CACHE_VERSION =
|
|
33
|
+
var CONCEPT_CACHE_VERSION = 2;
|
|
34
34
|
var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
35
35
|
var CONCEPT_TIMEOUT_MS = 10 * 60000;
|
|
36
36
|
var MAX_CONCEPT_TIMEOUT_MS = 60 * 60000;
|
|
@@ -222,7 +222,7 @@ async function enforceConceptCacheLimit(root, maximumBytes = CONCEPT_CACHE_MAX_B
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// src/concept-inference.ts
|
|
225
|
-
var INFERENCE_BATCH_SIZE =
|
|
225
|
+
var INFERENCE_BATCH_SIZE = 4;
|
|
226
226
|
function safeUtf16End(text, end) {
|
|
227
227
|
if (end <= 0 || end >= text.length)
|
|
228
228
|
return end;
|
|
@@ -247,6 +247,8 @@ function binarySearchBudget(span) {
|
|
|
247
247
|
return 2 * Math.max(span, 1) + 32;
|
|
248
248
|
}
|
|
249
249
|
function maximumTokenSafeEnd(extractor, prefix, text, start) {
|
|
250
|
+
if (tokenCount(extractor, `${prefix}${text.slice(start)}`) <= CONCEPT_MODEL_TOKENS)
|
|
251
|
+
return text.length;
|
|
250
252
|
let low = start + 1;
|
|
251
253
|
let high = text.length;
|
|
252
254
|
let accepted = start;
|
|
@@ -312,25 +314,49 @@ function tokenSafeWindows(extractor, pending) {
|
|
|
312
314
|
async function embedConceptInputs(extractor, pending, callbacks = {}) {
|
|
313
315
|
const layouts = pending.map((item) => tokenSafeWindows(extractor, item));
|
|
314
316
|
const inputs = layouts.flatMap((windows) => windows.map((window) => window.input));
|
|
315
|
-
const vectors =
|
|
317
|
+
const vectors = Array(inputs.length);
|
|
316
318
|
const layoutEnds = [];
|
|
319
|
+
const remainingWindows = layouts.map((windows) => windows.length);
|
|
317
320
|
let layoutEnd = 0;
|
|
318
321
|
for (const windows of layouts) {
|
|
319
322
|
layoutEnd += windows.length;
|
|
320
323
|
layoutEnds.push(layoutEnd);
|
|
321
324
|
}
|
|
325
|
+
const orderedInputs = inputs.map((input, index) => ({ input, index, tokens: tokenCount(extractor, input) })).toSorted((left, right) => right.tokens - left.tokens || left.index - right.index);
|
|
326
|
+
const vectorFor = (index) => {
|
|
327
|
+
const vector = vectors[index];
|
|
328
|
+
if (!vector)
|
|
329
|
+
throw new Error("Missing concept embedding vector");
|
|
330
|
+
return vector;
|
|
331
|
+
};
|
|
332
|
+
const ownerOf = (index) => {
|
|
333
|
+
let low = 0;
|
|
334
|
+
let high = layoutEnds.length;
|
|
335
|
+
while (low < high) {
|
|
336
|
+
const middle = Math.floor((low + high) / 2);
|
|
337
|
+
if (index < layoutEnds[middle])
|
|
338
|
+
high = middle;
|
|
339
|
+
else
|
|
340
|
+
low = middle + 1;
|
|
341
|
+
}
|
|
342
|
+
return low;
|
|
343
|
+
};
|
|
322
344
|
let completedEmbedding = 0;
|
|
323
|
-
|
|
324
|
-
|
|
345
|
+
let completedWindows = 0;
|
|
346
|
+
for (let offset = 0;offset < orderedInputs.length; ) {
|
|
347
|
+
const orderedBatch = orderedInputs.slice(offset, offset + INFERENCE_BATCH_SIZE);
|
|
348
|
+
const batch = orderedBatch.map((item) => item.input);
|
|
325
349
|
const tensor = await extractor(batch, { pooling: "mean", normalize: true });
|
|
326
350
|
if (tensor.data.length !== batch.length * CONCEPT_EMBEDDING_DIMENSIONS)
|
|
327
351
|
throw new Error("Unexpected concept embedding dimensions");
|
|
328
352
|
for (let index = 0;index < batch.length; index += 1) {
|
|
329
353
|
const start = index * CONCEPT_EMBEDDING_DIMENSIONS;
|
|
330
|
-
|
|
354
|
+
const inputIndex = orderedBatch[index].index;
|
|
355
|
+
vectors[inputIndex] = Array.from(tensor.data.slice(start, start + CONCEPT_EMBEDDING_DIMENSIONS), Number);
|
|
356
|
+
remainingWindows[ownerOf(inputIndex)]--;
|
|
331
357
|
}
|
|
332
|
-
|
|
333
|
-
while (
|
|
358
|
+
completedWindows += batch.length;
|
|
359
|
+
while (remainingWindows[completedEmbedding] !== undefined && remainingWindows[completedEmbedding] === 0) {
|
|
334
360
|
const start = completedEmbedding === 0 ? 0 : layoutEnds[completedEmbedding - 1];
|
|
335
361
|
const item = pending[completedEmbedding];
|
|
336
362
|
if (!item)
|
|
@@ -340,12 +366,13 @@ async function embedConceptInputs(extractor, pending, callbacks = {}) {
|
|
|
340
366
|
windows: (layouts[completedEmbedding] ?? []).map((window, index) => ({
|
|
341
367
|
start: window.start,
|
|
342
368
|
end: window.end,
|
|
343
|
-
vector:
|
|
369
|
+
vector: vectorFor(start + index)
|
|
344
370
|
}))
|
|
345
371
|
}, completedEmbedding + 1, pending.length);
|
|
346
372
|
completedEmbedding += 1;
|
|
347
373
|
}
|
|
348
374
|
callbacks.onBatch?.(completedWindows, inputs.length);
|
|
375
|
+
offset += batch.length;
|
|
349
376
|
}
|
|
350
377
|
let vectorIndex = 0;
|
|
351
378
|
return pending.map((item, index) => ({
|
|
@@ -353,7 +380,7 @@ async function embedConceptInputs(extractor, pending, callbacks = {}) {
|
|
|
353
380
|
windows: (layouts[index] ?? []).map((window) => ({
|
|
354
381
|
start: window.start,
|
|
355
382
|
end: window.end,
|
|
356
|
-
vector:
|
|
383
|
+
vector: vectorFor(vectorIndex++)
|
|
357
384
|
}))
|
|
358
385
|
}));
|
|
359
386
|
}
|
|
@@ -504,7 +531,7 @@ async function search() {
|
|
|
504
531
|
local_files_only: true,
|
|
505
532
|
dtype: "q8",
|
|
506
533
|
device: "cpu",
|
|
507
|
-
session_options: { intraOpNumThreads:
|
|
534
|
+
session_options: { intraOpNumThreads: 4, interOpNumThreads: 1 }
|
|
508
535
|
});
|
|
509
536
|
try {
|
|
510
537
|
created = await embedConceptInputs(extractor, missing, {
|
package/src/concept-worker.ts
CHANGED
|
@@ -140,7 +140,7 @@ async function search(): Promise<void> {
|
|
|
140
140
|
local_files_only: true,
|
|
141
141
|
dtype: "q8",
|
|
142
142
|
device: "cpu",
|
|
143
|
-
session_options: { intraOpNumThreads:
|
|
143
|
+
session_options: { intraOpNumThreads: 4, interOpNumThreads: 1 },
|
|
144
144
|
});
|
|
145
145
|
try {
|
|
146
146
|
created = await embedConceptInputs(extractor, missing, {
|
package/src/config-reader.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const SIFT_LIGHT_ENFORCEMENT_ENV = "SIFT_LIGHT_ENFORCE_SEARCH";
|
|
|
12
12
|
export interface SiftLightConfig {
|
|
13
13
|
locale: SiftLightLocale;
|
|
14
14
|
enforceSearch?: SearchEnforcementMode;
|
|
15
|
+
vectorSearchEnabled?: boolean;
|
|
15
16
|
semanticJudge?: SemanticJudgeConfig;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -44,12 +45,14 @@ export const DEFAULT_SEMANTIC_JUDGE_CONFIG: Readonly<SemanticJudgeConfig> = {
|
|
|
44
45
|
export const DEFAULT_SIFT_LIGHT_CONFIG: Readonly<SiftLightConfig> = {
|
|
45
46
|
locale: "en",
|
|
46
47
|
enforceSearch: "hard",
|
|
48
|
+
vectorSearchEnabled: false,
|
|
47
49
|
semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
interface RawSiftLightConfig {
|
|
51
53
|
locale?: unknown;
|
|
52
54
|
enforceSearch?: unknown;
|
|
55
|
+
vectorSearchEnabled?: unknown;
|
|
53
56
|
semanticJudge?: unknown;
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -221,21 +224,26 @@ function parseConfig(value: unknown, path: string): SiftLightConfig {
|
|
|
221
224
|
throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
|
|
222
225
|
}
|
|
223
226
|
const unknown = Object.keys(value).filter(
|
|
224
|
-
(key) => !["locale", "enforceSearch", "semanticJudge"].includes(key),
|
|
227
|
+
(key) => !["locale", "enforceSearch", "vectorSearchEnabled", "semanticJudge"].includes(key),
|
|
225
228
|
);
|
|
226
229
|
if (unknown.length > 0) {
|
|
227
230
|
throw new Error(
|
|
228
|
-
`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch and semanticJudge are accepted`,
|
|
231
|
+
`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled and semanticJudge are accepted`,
|
|
229
232
|
);
|
|
230
233
|
}
|
|
231
|
-
const { locale, enforceSearch } = value;
|
|
234
|
+
const { locale, enforceSearch, vectorSearchEnabled } = value;
|
|
232
235
|
if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
|
|
233
236
|
throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
|
|
234
237
|
}
|
|
238
|
+
if (vectorSearchEnabled !== undefined && typeof vectorSearchEnabled !== "boolean") {
|
|
239
|
+
throw new Error(`Invalid sift-light config at ${path}: vectorSearchEnabled must be a boolean`);
|
|
240
|
+
}
|
|
235
241
|
const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
|
|
236
242
|
return {
|
|
237
243
|
locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
|
|
238
244
|
enforceSearch: enforcement,
|
|
245
|
+
vectorSearchEnabled:
|
|
246
|
+
vectorSearchEnabled ?? DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled ?? false,
|
|
239
247
|
semanticJudge: parseSemanticJudge(value.semanticJudge, path),
|
|
240
248
|
};
|
|
241
249
|
}
|
package/src/hybrid-search.ts
CHANGED
|
@@ -274,6 +274,14 @@ export async function combineHybridSearch(
|
|
|
274
274
|
const selectionReason = conceptCandidatesOmitted
|
|
275
275
|
? `Hybrid concept limit retained the top ${String(selectedConcept.length)} of ${String(eligibleConcept.length)} non-overlapping semantic candidates`
|
|
276
276
|
: undefined;
|
|
277
|
+
const firstScore = Number(
|
|
278
|
+
eligibleConcept[0]?.details?.rankingScore ?? eligibleConcept[0]?.details?.score,
|
|
279
|
+
);
|
|
280
|
+
const secondScore = Number(
|
|
281
|
+
eligibleConcept[1]?.details?.rankingScore ?? eligibleConcept[1]?.details?.score,
|
|
282
|
+
);
|
|
283
|
+
const closeRanking =
|
|
284
|
+
Number.isFinite(firstScore) && Number.isFinite(secondScore) && firstScore - secondScore < 0.01;
|
|
277
285
|
return {
|
|
278
286
|
kind: "hybrid",
|
|
279
287
|
unit: "evidence-items",
|
|
@@ -285,6 +293,11 @@ export async function combineHybridSearch(
|
|
|
285
293
|
...literal.reasons,
|
|
286
294
|
...execution.sourceGeneration.reasons,
|
|
287
295
|
...(selectionReason ? [selectionReason] : []),
|
|
296
|
+
...(closeRanking
|
|
297
|
+
? [
|
|
298
|
+
"Semantic ranks are close; verify the leading candidates with source inspection or literal terms.",
|
|
299
|
+
]
|
|
300
|
+
: []),
|
|
288
301
|
...(judgedConcept.semanticJudge?.reason ? [judgedConcept.semanticJudge.reason] : []),
|
|
289
302
|
],
|
|
290
303
|
filesRead: (concept.filesRead ?? 0) + access.filesRead,
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { SIFT_LIGHT_DESCRIPTION, siftLightSchema } from "./tool-schema.js";
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
4
|
-
normalizeSearchEnforcement,
|
|
5
|
-
readSiftLightConfig,
|
|
6
|
-
DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
7
|
-
type SiftLightConfig,
|
|
8
|
-
} from "./config.js";
|
|
3
|
+
import { normalizeSearchEnforcement, readSiftLightConfig, type SiftLightConfig } from "./config.js";
|
|
9
4
|
import { resolveContextBudget } from "./context-budget.js";
|
|
10
5
|
import { createRipgrepRunner } from "./rg.js";
|
|
11
6
|
import { createCtagsStructureProvider } from "./structure.js";
|
|
@@ -17,7 +12,7 @@ import type { SiftLightDetails } from "./types.js";
|
|
|
17
12
|
import { renderSiftLightCall, renderSiftLightResult } from "./tui/renderers.js";
|
|
18
13
|
import { registerPiSearchPolicy } from "./pi-search-policy.js";
|
|
19
14
|
import { modelErrorText } from "./model-error.js";
|
|
20
|
-
import {
|
|
15
|
+
import { createConfiguredSemanticJudgeIntegration } from "./semantic-judge.js";
|
|
21
16
|
|
|
22
17
|
const SIFT_LIGHT_LABEL = "sift-light";
|
|
23
18
|
|
|
@@ -27,13 +22,12 @@ export async function registerSiftLightExtension(
|
|
|
27
22
|
pi: ExtensionAPI,
|
|
28
23
|
config: SiftLightConfig,
|
|
29
24
|
): Promise<void> {
|
|
30
|
-
const semanticJudge =
|
|
31
|
-
config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
32
|
-
);
|
|
25
|
+
const semanticJudge = createConfiguredSemanticJudgeIntegration(config);
|
|
33
26
|
const runtime = new SiftLightRuntime(
|
|
34
27
|
new SiftLightService({
|
|
35
28
|
runRipgrep: createRipgrepRunner(),
|
|
36
29
|
structure: createCtagsStructureProvider(),
|
|
30
|
+
vectorSearchEnabled: config.vectorSearchEnabled === true,
|
|
37
31
|
semanticJudge,
|
|
38
32
|
}),
|
|
39
33
|
);
|
package/src/mcp-model-output.ts
CHANGED
|
@@ -7,7 +7,48 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
7
7
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function compactSemanticMetadata(details: SiftLightDetails, analysis: AnalysisDetails): string[] {
|
|
11
|
+
const counts = analysis.counts;
|
|
12
|
+
const stats = analysis.stats;
|
|
13
|
+
const judge = analysis.semanticJudge;
|
|
14
|
+
const coverage = analysis.coverage;
|
|
15
|
+
return [
|
|
16
|
+
...(counts || stats
|
|
17
|
+
? [
|
|
18
|
+
`Search: ${String(counts?.filesAdmitted ?? stats?.filesAdmitted ?? 0)} files, ${String(stats?.passagesRanked ?? counts?.passagesQueued ?? 0)} passages; ${String(stats?.elapsedMs ?? 0)} ms; peak inference RSS ${String(stats?.inferencePeakRssBytes ?? 0)} bytes; cache ${String(stats?.conceptCacheHits ?? 0)} hits/${String(stats?.conceptCacheMisses ?? 0)} misses.`,
|
|
19
|
+
]
|
|
20
|
+
: []),
|
|
21
|
+
...(coverage
|
|
22
|
+
? [
|
|
23
|
+
`Coverage: ${Object.entries(coverage)
|
|
24
|
+
.map(([name, status]) => `${name}=${status}`)
|
|
25
|
+
.join(", ")}.`,
|
|
26
|
+
]
|
|
27
|
+
: []),
|
|
28
|
+
...(analysis.scope
|
|
29
|
+
? [`Scope: ${analysis.scope.path}; ignore=${analysis.scope.ignorePolicy}.`]
|
|
30
|
+
: []),
|
|
31
|
+
...(analysis.sourceGeneration
|
|
32
|
+
? [
|
|
33
|
+
`Source: ${analysis.sourceGeneration.verification}; ${String(analysis.sourceGeneration.filesUnavailable)} unavailable.`,
|
|
34
|
+
]
|
|
35
|
+
: []),
|
|
36
|
+
...(judge
|
|
37
|
+
? [
|
|
38
|
+
`Semantic judge: ${judge.status}; ${String(judge.judgedCandidates)}/${String(judge.candidatesConsidered)} judged, ${String(judge.candidatesUnjudged)} unjudged; batches ${String(judge.batchesCompleted)}/${String(judge.batchesAttempted)} completed; classes ${JSON.stringify(judge.classificationCounts)}${judge.reason ? `; ${judge.reason}` : ""}.`,
|
|
39
|
+
]
|
|
40
|
+
: []),
|
|
41
|
+
...(details.operation
|
|
42
|
+
? [`Operation: ${details.operation.state}; id=${details.operation.id}.`]
|
|
43
|
+
: []),
|
|
44
|
+
...analysis.reasons.map((reason) => `[${reason}]`),
|
|
45
|
+
...(details.redactionApplied ? ["[Display redaction applied.]"] : []),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
10
49
|
function compactMetadata(details: SiftLightDetails, analysis: AnalysisDetails): string[] {
|
|
50
|
+
if (analysis.kind === "concept" || analysis.kind === "hybrid")
|
|
51
|
+
return compactSemanticMetadata(details, analysis);
|
|
11
52
|
return [
|
|
12
53
|
...(analysis.statistics ? formatStatistics(analysis.statistics) : []),
|
|
13
54
|
analysis.counts ? `Counts: ${JSON.stringify(analysis.counts)}` : undefined,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_SIFT_LIGHT_CONFIG,
|
|
2
3
|
DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
3
4
|
readSiftLightConfigFile,
|
|
4
5
|
SIFT_LIGHT_CONFIG_ENV,
|
|
5
6
|
} from "./config-reader.js";
|
|
6
7
|
import {
|
|
7
8
|
createDisabledSemanticJudgeIntegration,
|
|
8
|
-
|
|
9
|
+
createConfiguredSemanticJudgeIntegration,
|
|
9
10
|
type SemanticJudgeIntegration,
|
|
10
11
|
} from "./semantic-judge.js";
|
|
11
12
|
|
|
@@ -19,17 +20,27 @@ function configuredPath(environment: NodeJS.ProcessEnv): string | undefined {
|
|
|
19
20
|
* The MCP process must receive the same config path explicitly in its own host
|
|
20
21
|
* environment; an absent path remains an observable, local-only disabled state.
|
|
21
22
|
*/
|
|
22
|
-
export async function
|
|
23
|
+
export async function createMcpSearchFeatures(
|
|
23
24
|
environment: NodeJS.ProcessEnv = process.env,
|
|
24
|
-
): Promise<SemanticJudgeIntegration> {
|
|
25
|
+
): Promise<{ semanticJudge: SemanticJudgeIntegration; vectorSearchEnabled: boolean }> {
|
|
25
26
|
const path = configuredPath(environment);
|
|
26
|
-
if (!path)
|
|
27
|
+
if (!path)
|
|
28
|
+
return {
|
|
29
|
+
semanticJudge: createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG),
|
|
30
|
+
vectorSearchEnabled: DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled === true,
|
|
31
|
+
};
|
|
27
32
|
|
|
28
33
|
const config = await readSiftLightConfigFile(path, { missing: "error" });
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
return {
|
|
35
|
+
semanticJudge: createConfiguredSemanticJudgeIntegration(config, environment),
|
|
36
|
+
vectorSearchEnabled: config.vectorSearchEnabled === true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function createMcpSemanticJudgeIntegration(
|
|
41
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
42
|
+
): Promise<SemanticJudgeIntegration> {
|
|
43
|
+
return (await createMcpSearchFeatures(environment)).semanticJudge;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
export function mcpSemanticJudgeConfigSource(
|