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/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.1 — 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Local vector search is now off by default. Existing `concept` and `hybrid` calls require `"vectorSearchEnabled": true` in the active `sift-light.json` and an installed model; otherwise they fail explicitly. Ordinary search remains available. Installing the model or enabling the optional Jev judge alone does not opt in.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Improve semantic ranking when very short passages compete with more detailed source evidence. Raw cosine similarity remains available alongside the ranking score; results are still relevance candidates, not verified behavior.
|
|
12
|
+
- Give `files` requests that mistakenly use a plain `pattern` a complete, scope-preserving `query` recovery request. Reduce repeated request-error text.
|
|
13
|
+
- Condense hybrid and Concept diagnostics in model-facing output while keeping coverage, progress, recovery, and semantic-judge status visible.
|
|
14
|
+
- Ask the optional Jev semantic judge to classify each candidate by its own excerpt, avoiding a shared classification instruction across candidates.
|
|
15
|
+
|
|
16
|
+
### Performance and compatibility
|
|
17
|
+
|
|
18
|
+
- Use length-aware inference ordering, batches of four, four ONNX CPU threads, and a shorter token window to reduce cold semantic search time on the tested workloads. The changed windowing invalidates older embedding cache entries once; subsequent searches refill the cache.
|
|
19
|
+
- Cold semantic inference can still take tens of seconds and use more than 1 GiB of worker memory. The improvement is workload-dependent and is not a memory cap or a subsecond cold-start guarantee.
|
package/README.md
CHANGED
|
@@ -24,12 +24,18 @@ Looking for an error message, a sentence or a name is like giving a librarian a
|
|
|
24
24
|
|
|
25
25
|
### Recover when the remembered wording is not exact
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
For routine development work, searches with a known name, symbol, filename or error text use the fast exact path by default; omitting `mode` does not load the local embedding model. Vector search is disabled by default for Pi, OMP and MCP clients. `concept` and `hybrid` fail clearly while it is disabled; neither silently becomes an exact-only search. This behavior is built into the plugin and does not depend on a personal `AGENTS.md`. An uncached semantic search can take tens of seconds, so it should not be part of every code lookup.
|
|
28
|
+
|
|
29
|
+
To opt in, set `"vectorSearchEnabled": true` in the active `sift-light.json`, install the local model with `npx -y --package sift-light@latest sift-light-model --install-model`, and restart the host. Pi and OMP use the configuration paths below; MCP needs `SIFT_LIGHT_CONFIG` set to that file in its server environment. Installing the model alone does not enable vector search. `semanticJudge.enabled` is a separate option for remote candidate classification: it does not enable vector search and remains inactive while vector search is disabled.
|
|
30
|
+
|
|
31
|
+
Use `mode: "hybrid"` with one natural-language `query` when a sentence may have been remembered with different wording. Hybrid always runs an exact literal search and the installed local Concept model under one owned request. Exact evidence appears first; semantic candidates are clearly labeled as relevance candidates and removed when they overlap an exact match. Semantic candidates retain raw cosine scores and use a bounded short-passage correction for ranking; both scores remain visible in structured details and neither proves runtime behavior. The initial page shares counts, coverage, source references, one inspection cursor and a compact preview instead of concatenating two complete responses. `conceptLimit` changes only the non-overlapping semantic supplement (default 3, maximum 20); it never displaces literal evidence. The returned matches request opens the same snapshot's complete exact-first pagination without rerunning either search.
|
|
28
32
|
|
|
29
33
|
Concept ranking covers every UTF-8 passage admitted by the request's documented source budget; it no longer samples a fixed prefix of the scope. Concept and hybrid searches automatically admit up to 2,000 files and process them sequentially in bounded 200-file batches, then merge every batch into one global ranking and one coverage result. Batches share the request's 32 MiB read budget, so raising the file ceiling does not multiply the content budget. Users do not need to plan or continue batches themselves. `maxFilesToParse` remains an optional advanced hard ceiling when a smaller scope is intentional. Passages that exceed the model token window are ranked through overlapping token-safe windows, so later text is not silently discarded. Offline embeddings are cached by content, model revision and chunking revision in a bounded 512 MiB local cache. Repeated content is reused, changed content misses naturally, and cache write or cleanup failures remain visible in the result.
|
|
30
34
|
|
|
31
35
|
Slow Concept and hybrid requests return within the default five-second wait window with `status: "waiting"` or `"running"`, an `operationId`, progress and an exact `nextRequest` such as `{ "mode": "await", "operationId": "..." }`. Copy that request unchanged: it resumes the same computation and never restarts the query or downgrades to a literal-only result. A final result remains available for stable re-fetch for 10 minutes, with up to 32 terminal results retained per service session, and `mode: "cancel"` stops the owned work and waits for cleanup. Each service session admits at most eight pending operations; an operation has one total deadline controlled by `SIFT_LIGHT_CONCEPT_TIMEOUT_MS` (integer milliseconds from 1000 through 3600000; default 600000) and a 120-second idle continuation lease. A real model, source or resource failure is returned as a failure with its diagnostic. Source generation is re-enumerated and re-verified before publication, so changes refresh the operation and mixed versions are never marked complete. Admission planning counts (`filesEnumerated`, `filesAdmitted`, `filesSkippedEmpty`, `filesUnavailable`, `passagesQueued`, `batchesPlanned`, `batchesCompleted`) stay visible. Empty files are a normal skip and do not mark the result partial.
|
|
32
36
|
|
|
37
|
+
The first uncached Concept or hybrid search loads the local model and may take tens of seconds and more than 1 GiB of inference-worker memory, depending on the machine and search scope. Later searches reuse cached passage embeddings even when the query wording changes, although a new query still loads the model briefly to embed that query. Changed passages are recomputed; cache eviction, deletion or broad source changes can cause another cold run. Exact search modes do not load the model. These are workload-dependent observations, not a latency or memory guarantee. If the result is waiting during `model-loading`, follow its `nextRequest` to continue the same operation.
|
|
38
|
+
|
|
33
39
|
### Give several search conditions together
|
|
34
40
|
|
|
35
41
|
“Find files mentioning both the customer and a refund” works like selecting documents with two labels. “Any of these words will do” works like handing over a shortlist. Multiple conditions can be expressed together to reduce repeated searches.
|
|
@@ -120,6 +126,7 @@ The feature is enabled only when `semanticJudge.enabled` is explicitly set to `t
|
|
|
120
126
|
{
|
|
121
127
|
"locale": "en",
|
|
122
128
|
"enforceSearch": "hard",
|
|
129
|
+
"vectorSearchEnabled": false,
|
|
123
130
|
"semanticJudge": {
|
|
124
131
|
"enabled": false,
|
|
125
132
|
"provider": "jev",
|
package/README.zh-CN.md
CHANGED
|
@@ -24,12 +24,18 @@
|
|
|
24
24
|
|
|
25
25
|
### 记得不完全准确,也能一次找回
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
日常开发时,已知名称、符号、文件名或报错文本的搜索默认走快速的精确路径;省略 `mode` 不会加载本地向量模型。Pi、OMP 和 MCP 的向量搜索默认关闭;关闭时 `concept` 和 `hybrid` 会明确报错,不会悄悄降级成只有精确结果。这是插件自身的行为,不依赖个人的 `AGENTS.md`。未缓存的语义搜索可能耗时数十秒,不应成为每次查代码的默认成本。
|
|
28
|
+
|
|
29
|
+
需要时,在当前生效的 `sift-light.json` 中设置 `"vectorSearchEnabled": true`,执行 `npx -y --package sift-light@latest sift-light-model --install-model` 安装本地模型,然后重启宿主。Pi 和 OMP 的配置路径见下文;MCP 需要在服务进程环境中用 `SIFT_LIGHT_CONFIG` 指向该文件。只安装模型不会自动启用向量搜索。`semanticJudge.enabled` 是远程候选分类的独立开关,不会启用向量搜索;向量关闭时它也不会运行。
|
|
30
|
+
|
|
31
|
+
当一句话可能记错了措辞时,可以用一个自然语言 `query` 调用 `mode: "hybrid"`。Hybrid 会在同一个受控请求中始终执行精确字面搜索和已安装的本地 Concept 模型:精确证据固定排在前面,语义候选明确标注且只表示相关性候选,与精确命中范围重叠的候选会被去重。语义候选保留原始余弦分数,并用有界短段修正分数排序;两种分数均在结构化详情中可见,不能当作运行时证明。初始页面共享计数、覆盖状态、来源引用和一个检查游标,以紧凑预览代替拼接两份完整响应。`conceptLimit` 只调整不重叠的语义补充数量(默认 3,最大 20),不会挤占字面证据;返回的 matches 请求从同一个快照开始完整的精确优先分页,不会重新执行任一搜索。
|
|
28
32
|
|
|
29
33
|
Concept 排名会覆盖请求所声明源码预算内接纳的全部 UTF-8 段落,不再固定抽取范围开头的一小部分。Concept 和 hybrid 默认会自动接纳最多 2,000 个文件,在内部按每批 200 个文件顺序处理,再合并成一次全局排名和一份覆盖结果。所有批次共享同一个请求的 32 MiB 读取预算,扩大文件上限不会把内容预算成倍放大。用户不需要自己计算或续接批次;只有确实想主动缩小范围时,才需要把 `maxFilesToParse` 作为可选的高级硬上限。超过模型 token 窗口的段落会拆成带重叠、且保证不截断的窗口参与排名,后半段内容不会被静默丢弃。离线 embedding 按内容、模型版本和分段版本缓存在本地,缓存上限为 512 MiB;重复内容直接复用,内容变化自然失效,缓存写入或清理失败会在结果中明确显示。
|
|
30
34
|
|
|
31
35
|
Concept 或 hybrid 较慢时,会在默认五秒等待窗口内返回 `status: "waiting"` 或 `"running"`、`operationId`、进度和精确的 `nextRequest`,例如 `{ "mode": "await", "operationId": "..." }`。请原样复制这个请求:它会续接同一个计算,不会重启查询,也不会降级成只有字面的结果。最终结果可稳定复取十分钟;每个服务会话最多保留 32 个终态结果。`mode: "cancel"` 会停止自有任务并等待清理完成。每个服务会话最多同时接纳八个 pending operation;单个 operation 使用 `SIFT_LIGHT_CONCEPT_TIMEOUT_MS` 指定一个总执行时限(整数毫秒,1000–3600000,默认 600000),另有 120 秒无人续接租期。真实模型、来源或资源故障会以明确失败返回。发布结果前会重新枚举并校验同一来源 generation;源文件变化会刷新 operation,混合版本不会被标成 complete。接纳计划计数(`filesEnumerated`、`filesAdmitted`、`filesSkippedEmpty`、`filesUnavailable`、`passagesQueued`、`batchesPlanned`、`batchesCompleted`)会在结果里明确显示。空文件属于正常跳过,不会把结果标成 partial。
|
|
32
36
|
|
|
37
|
+
首次运行尚未缓存的 Concept 或 hybrid 搜索时需要加载本地模型;耗时可能达到数十秒,推理 worker 内存也可能超过 1 GiB,具体取决于机器和搜索范围。后续搜索即使换了问题,也会复用已缓存的段落向量;新问题仍会短暂加载模型来计算问题向量。修改过的段落需要重算;缓存被清理、淘汰或大量源码变化时可能再次冷运行。精确搜索模式不会加载模型。这些都是随负载变化的观察,不是延迟或内存保证。结果在 `model-loading` 阶段等待时,请按返回的 `nextRequest` 续接同一个 operation。
|
|
38
|
+
|
|
33
39
|
### 几个条件,可以一起交代
|
|
34
40
|
|
|
35
41
|
“找同时提到客户和退款的文件”,就像请管理员挑出同时贴着两张标签的资料;“这几个词任意一个出现都算”,则像列出一张候选清单。可以一次表达多个查找条件,减少反复搜索。
|
|
@@ -120,6 +126,7 @@ Hybrid 搜索默认只使用本地能力。可选的语义判断器可以对保
|
|
|
120
126
|
{
|
|
121
127
|
"locale": "zh-CN",
|
|
122
128
|
"enforceSearch": "hard",
|
|
129
|
+
"vectorSearchEnabled": false,
|
|
123
130
|
"semanticJudge": {
|
|
124
131
|
"enabled": false,
|
|
125
132
|
"provider": "jev",
|
package/package.json
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, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sift-light",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Require sift-light for conventional local searches while keeping development tools available.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "baoer"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"args": [
|
|
14
14
|
"--yes",
|
|
15
15
|
"--package",
|
|
16
|
-
"sift-light-runtime@npm:sift-light@1.0.
|
|
16
|
+
"sift-light-runtime@npm:sift-light@1.0.1",
|
|
17
17
|
"sift-light-mcp",
|
|
18
18
|
"--stdio"
|
|
19
19
|
],
|
|
@@ -71,6 +71,7 @@ var DEFAULT_SEMANTIC_JUDGE_CONFIG = {
|
|
|
71
71
|
var DEFAULT_SIFT_LIGHT_CONFIG = {
|
|
72
72
|
locale: "en",
|
|
73
73
|
enforceSearch: "hard",
|
|
74
|
+
vectorSearchEnabled: false,
|
|
74
75
|
semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG
|
|
75
76
|
};
|
|
76
77
|
function resolveSiftLightConfigPath(agentDirectory, environment = process.env) {
|
|
@@ -163,18 +164,22 @@ function parseConfig(value, path) {
|
|
|
163
164
|
if (!isRawSiftLightConfig(value)) {
|
|
164
165
|
throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
|
|
165
166
|
}
|
|
166
|
-
const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "semanticJudge"].includes(key));
|
|
167
|
+
const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "vectorSearchEnabled", "semanticJudge"].includes(key));
|
|
167
168
|
if (unknown.length > 0) {
|
|
168
|
-
throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch and semanticJudge are accepted`);
|
|
169
|
+
throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled and semanticJudge are accepted`);
|
|
169
170
|
}
|
|
170
|
-
const { locale, enforceSearch } = value;
|
|
171
|
+
const { locale, enforceSearch, vectorSearchEnabled } = value;
|
|
171
172
|
if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
|
|
172
173
|
throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
|
|
173
174
|
}
|
|
175
|
+
if (vectorSearchEnabled !== undefined && typeof vectorSearchEnabled !== "boolean") {
|
|
176
|
+
throw new Error(`Invalid sift-light config at ${path}: vectorSearchEnabled must be a boolean`);
|
|
177
|
+
}
|
|
174
178
|
const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
|
|
175
179
|
return {
|
|
176
180
|
locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
|
|
177
181
|
enforceSearch: enforcement,
|
|
182
|
+
vectorSearchEnabled: vectorSearchEnabled ?? DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled ?? false,
|
|
178
183
|
semanticJudge: parseSemanticJudge(value.semanticJudge, path)
|
|
179
184
|
};
|
|
180
185
|
}
|
|
@@ -1814,7 +1819,7 @@ function createCtagsStructureProvider(options = {}) {
|
|
|
1814
1819
|
// package.json
|
|
1815
1820
|
var package_default = {
|
|
1816
1821
|
name: "sift-light",
|
|
1817
|
-
version: "1.0.
|
|
1822
|
+
version: "1.0.1",
|
|
1818
1823
|
description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
|
|
1819
1824
|
keywords: [
|
|
1820
1825
|
"ai-agent",
|
|
@@ -2185,7 +2190,7 @@ var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
|
|
|
2185
2190
|
var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
|
|
2186
2191
|
var MAX_CONCEPT_CHARS = 1000;
|
|
2187
2192
|
var CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
|
|
2188
|
-
var CONCEPT_CACHE_VERSION =
|
|
2193
|
+
var CONCEPT_CACHE_VERSION = 2;
|
|
2189
2194
|
var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
2190
2195
|
var CONCEPT_TIMEOUT_MS = 10 * 60000;
|
|
2191
2196
|
var MIN_CONCEPT_TIMEOUT_MS = 1000;
|
|
@@ -4687,6 +4692,11 @@ function scoreProfile(scores) {
|
|
|
4687
4692
|
spread: top - min
|
|
4688
4693
|
};
|
|
4689
4694
|
}
|
|
4695
|
+
function conceptRankingScore(cosine, passageLength) {
|
|
4696
|
+
const referenceLength = 500;
|
|
4697
|
+
const maximumCorrection = 0.02;
|
|
4698
|
+
return cosine - maximumCorrection * (1 - Math.sqrt(Math.min(passageLength / referenceLength, 1)));
|
|
4699
|
+
}
|
|
4690
4700
|
function passage(document2, start2) {
|
|
4691
4701
|
let end = Math.min(document2.text.length, start2 + MAX_CONCEPT_CHARS);
|
|
4692
4702
|
if (end < document2.text.length) {
|
|
@@ -4945,6 +4955,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
4945
4955
|
const similarity = inferred.scores[index];
|
|
4946
4956
|
if (similarity === undefined)
|
|
4947
4957
|
throw new Error("Missing concept similarity");
|
|
4958
|
+
const rankingScore = conceptRankingScore(similarity, item.text.length);
|
|
4948
4959
|
const evidence = rangeEvidence(item.document, item.range);
|
|
4949
4960
|
return {
|
|
4950
4961
|
path: item.document.path,
|
|
@@ -4957,7 +4968,8 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
4957
4968
|
kind: "concept-candidate",
|
|
4958
4969
|
certainty: "candidate",
|
|
4959
4970
|
score: similarity,
|
|
4960
|
-
|
|
4971
|
+
rankingScore,
|
|
4972
|
+
rankingReason: "local multilingual E5 cosine similarity with bounded short-passage rank correction; relevance candidate, no binding or execution claim",
|
|
4961
4973
|
model: CONCEPT_MODEL,
|
|
4962
4974
|
revision: CONCEPT_REVISION,
|
|
4963
4975
|
tokenTruncated: false,
|
|
@@ -4966,7 +4978,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
4966
4978
|
}
|
|
4967
4979
|
};
|
|
4968
4980
|
});
|
|
4969
|
-
const ranked = [...result.items, ...batchItems].toSorted((a, b) => Number(b.details?.
|
|
4981
|
+
const ranked = [...result.items, ...batchItems].toSorted((a, b) => Number(b.details?.rankingScore) - Number(a.details?.rankingScore) || a.path.localeCompare(b.path) || a.line - b.line);
|
|
4970
4982
|
if (ranked.length > MAX_ANALYSIS_RESULTS)
|
|
4971
4983
|
retentionTruncated = true;
|
|
4972
4984
|
result.items = ranked.slice(0, MAX_ANALYSIS_RESULTS);
|
|
@@ -9312,7 +9324,7 @@ function requestBody(query, candidates, model) {
|
|
|
9312
9324
|
for (const candidate of candidates) {
|
|
9313
9325
|
questions[candidate.id] = {
|
|
9314
9326
|
type: "choice",
|
|
9315
|
-
instructions:
|
|
9327
|
+
instructions: `Classify only candidate ${candidate.id} in state.candidates for state.query. Judge that candidate's excerpt by its actual behavior, not by matching words or the other candidates.`,
|
|
9316
9328
|
criteria: {
|
|
9317
9329
|
"implementation-candidate": "The excerpt appears to implement the requested behavior or its core decision/side effect.",
|
|
9318
9330
|
"caller-candidate": "The excerpt invokes or wires an implementation but does not implement the behavior itself.",
|
|
@@ -9475,6 +9487,10 @@ function createSemanticJudgeIntegration(config, environment = process.env, fetch
|
|
|
9475
9487
|
}
|
|
9476
9488
|
return { config, runner: createJevRunner(config, key, fetcher) };
|
|
9477
9489
|
}
|
|
9490
|
+
function createConfiguredSemanticJudgeIntegration(config, environment = process.env) {
|
|
9491
|
+
const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
|
|
9492
|
+
return config.vectorSearchEnabled === true ? createSemanticJudgeIntegration(judge, environment) : createDisabledSemanticJudgeIntegration(judge);
|
|
9493
|
+
}
|
|
9478
9494
|
function baseDetails(config) {
|
|
9479
9495
|
return {
|
|
9480
9496
|
enabled: config.enabled,
|
|
@@ -9781,6 +9797,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
|
|
|
9781
9797
|
const deduplicationCoverage = scan.snapshotComplete && literal.sourceCoverage === "complete" && conceptSourceCoverage === "complete" ? "complete" : "partial";
|
|
9782
9798
|
const partial = !scan.snapshotComplete || concept.partial || conceptCoverage === "skipped" || conceptSourceCoverage === "partial" || literal.sourceCoverage === "partial" || deduplicationCoverage === "partial" || judgedConcept.semanticJudge?.status === "failed" || judgedConcept.semanticJudge?.status === "partial";
|
|
9783
9799
|
const selectionReason = conceptCandidatesOmitted ? `Hybrid concept limit retained the top ${String(selectedConcept.length)} of ${String(eligibleConcept.length)} non-overlapping semantic candidates` : undefined;
|
|
9800
|
+
const firstScore = Number(eligibleConcept[0]?.details?.rankingScore ?? eligibleConcept[0]?.details?.score);
|
|
9801
|
+
const secondScore = Number(eligibleConcept[1]?.details?.rankingScore ?? eligibleConcept[1]?.details?.score);
|
|
9802
|
+
const closeRanking = Number.isFinite(firstScore) && Number.isFinite(secondScore) && firstScore - secondScore < 0.01;
|
|
9784
9803
|
return {
|
|
9785
9804
|
kind: "hybrid",
|
|
9786
9805
|
unit: "evidence-items",
|
|
@@ -9792,6 +9811,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
|
|
|
9792
9811
|
...literal.reasons,
|
|
9793
9812
|
...execution.sourceGeneration.reasons,
|
|
9794
9813
|
...selectionReason ? [selectionReason] : [],
|
|
9814
|
+
...closeRanking ? [
|
|
9815
|
+
"Semantic ranks are close; verify the leading candidates with source inspection or literal terms."
|
|
9816
|
+
] : [],
|
|
9795
9817
|
...judgedConcept.semanticJudge?.reason ? [judgedConcept.semanticJudge.reason] : []
|
|
9796
9818
|
],
|
|
9797
9819
|
filesRead: (concept.filesRead ?? 0) + access.filesRead,
|
|
@@ -10215,15 +10237,23 @@ function schemaError(input, field, reason) {
|
|
|
10215
10237
|
recovery: { action: "manual", reason }
|
|
10216
10238
|
}, `${field} is invalid: ${reason}`);
|
|
10217
10239
|
}
|
|
10240
|
+
function plainFilePatternRecovery(mode, input, invalid) {
|
|
10241
|
+
return mode === "files" && invalid.includes("pattern") && input.query === undefined && typeof input.pattern === "string" && input.pattern.trim().length > 0 && input.pattern.length <= 256 && input.pattern.isWellFormed() && !/[\\^$.*+?()[\]{}|\r\n\0]/u.test(input.pattern);
|
|
10242
|
+
}
|
|
10218
10243
|
function safeNextRequest(input, mode, invalid) {
|
|
10219
10244
|
if (input.redact === true && containsSensitiveText(input))
|
|
10220
10245
|
return;
|
|
10221
10246
|
const safe = new Set(SAFE_DROP_FIELDS[mode] ?? []);
|
|
10247
|
+
const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
|
|
10248
|
+
if (plainFilePattern)
|
|
10249
|
+
safe.add("pattern");
|
|
10222
10250
|
if (invalid.some((field) => !safe.has(field)))
|
|
10223
10251
|
return;
|
|
10224
10252
|
const next = { ...input };
|
|
10225
10253
|
for (const field of invalid)
|
|
10226
10254
|
delete next[field];
|
|
10255
|
+
if (plainFilePattern)
|
|
10256
|
+
next.query = input.pattern;
|
|
10227
10257
|
if (selectorIssuesFor(next, mode).length > 0)
|
|
10228
10258
|
return;
|
|
10229
10259
|
try {
|
|
@@ -10252,20 +10282,26 @@ function fieldsError(input, mode, invalid, selectorIssues = []) {
|
|
|
10252
10282
|
});
|
|
10253
10283
|
const visibleFields = visibleInvalid.map((field) => boundedField(field)).join(", ");
|
|
10254
10284
|
const reason = omitted > 0 ? `mode=${mode} does not accept ${visibleFields} and ${String(omitted)} additional field(s)` : `mode=${mode} does not accept ${visibleFields}`;
|
|
10255
|
-
const
|
|
10285
|
+
const nestedModeObject = invalid.includes(mode);
|
|
10286
|
+
const flatRule = nestedModeObject ? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.` : "";
|
|
10287
|
+
const filesPatternRule = mode === "files" && invalid.includes("pattern") ? "For filename or path discovery, put the literal name text in query; pattern is a content-search regex, so check any regex syntax before copying it." : "";
|
|
10256
10288
|
return new RequestContractError({
|
|
10257
10289
|
code: "E_MODE_FIELDS",
|
|
10258
10290
|
mode,
|
|
10259
10291
|
issues,
|
|
10260
10292
|
recovery: nextRequest ? {
|
|
10261
10293
|
action: "retry",
|
|
10262
|
-
reason: `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
|
|
10294
|
+
reason: plainFilePatternRecovery(mode, input, invalid) ? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved." : `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
|
|
10263
10295
|
nextRequest
|
|
10264
10296
|
} : {
|
|
10265
10297
|
action: "manual",
|
|
10266
|
-
reason:
|
|
10298
|
+
reason: [
|
|
10299
|
+
reason,
|
|
10300
|
+
filesPatternRule || "Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
|
|
10301
|
+
flatRule
|
|
10302
|
+
].filter(Boolean).join(" ")
|
|
10267
10303
|
}
|
|
10268
|
-
}, nextRequest ? `${reason};
|
|
10304
|
+
}, nextRequest ? `${reason}; ${plainFilePatternRecovery(mode, input, invalid) ? "move plain filename text to query" : "retry the exact nextRequest"}.` : `${reason}; no semantics-preserving automatic request is available. ${filesPatternRule || flatRule || "Check the mode's accepted fields."}`);
|
|
10269
10305
|
}
|
|
10270
10306
|
function selectorIssuesFor(input, mode) {
|
|
10271
10307
|
if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof input.cursor === "string" && input.cursor.trim().length > 0 && !input.cursor.includes(".analysis")) {
|
|
@@ -12743,6 +12779,7 @@ class SiftLightService {
|
|
|
12743
12779
|
#runRipgrep;
|
|
12744
12780
|
#snapshots;
|
|
12745
12781
|
#summaryFileLimit;
|
|
12782
|
+
#vectorSearchEnabled;
|
|
12746
12783
|
#capabilities = new LanguageCapabilityCatalog;
|
|
12747
12784
|
#evidence;
|
|
12748
12785
|
#lifecycle = new AbortController;
|
|
@@ -12753,12 +12790,16 @@ class SiftLightService {
|
|
|
12753
12790
|
this.#runRipgrep = options.runRipgrep;
|
|
12754
12791
|
this.#snapshots = options.snapshots ?? new SnapshotStore;
|
|
12755
12792
|
this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
|
|
12793
|
+
this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
|
|
12756
12794
|
this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
|
|
12757
12795
|
this.#evidence = new EvidenceService(this.#runRipgrep, this.#snapshots, options.structure, options.conceptSearch, options.semanticJudge);
|
|
12758
12796
|
}
|
|
12759
12797
|
async search(input, cwd, signal, options = {}) {
|
|
12760
12798
|
validateRawSearchInput(input);
|
|
12761
12799
|
validateRequestContract(input);
|
|
12800
|
+
if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
|
|
12801
|
+
throw new SiftLightError(`${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`);
|
|
12802
|
+
}
|
|
12762
12803
|
let request;
|
|
12763
12804
|
if (input.mode === "await" || input.mode === "cancel") {
|
|
12764
12805
|
request = this.#operationCommand(input, cwd, signal);
|
|
@@ -13161,7 +13202,7 @@ ${page.body}${rangeNote}${contextNote}${missingSelectionNote}
|
|
|
13161
13202
|
var SOURCE_OUTPUT_GUIDANCE = "Auto/summary text may include bounded source excerpts; ordinary matches text is metadata-only. Inspect may return bounded source windows covering an entire small file. Analysis text may include semantic passages; structured details may retain excerpts, names and signatures. Follow output limits, coverage and continuations.";
|
|
13162
13203
|
function siftLightPromptGuidelines(structuredOutput = true) {
|
|
13163
13204
|
return [
|
|
13164
|
-
`Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
|
|
13205
|
+
`Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} For routine development searches, start with fast exact content, filename or applicable structural modes when the request has a usable name, symbol, error text or other literal clue. Omitted mode is ordinary exact search and never loads the local embedding model. Vector search is disabled by default; concept/hybrid require vectorSearchEnabled:true in sift-light.json and an installed model. They can take tens of seconds on an uncached scope, so select them only when semantic recall is needed. Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
|
|
13165
13206
|
`An omitted path searches the project cwd. Use scope:"strict" for a question restricted to one path; otherwise, if an explicit subpath has zero matches, ordinary and content-analysis searches retry from cwd and return project-wide counts with an expansion notice. Explicit absolute paths and .. traversal can search outside cwd, except protected external system areas and .git internals. Git changes mode remains cwd-scoped.`,
|
|
13166
13207
|
`Search output includes counts, categories, ranked paths, coverage and continuation metadata. Source excerpts may contain the searched text. Use mode="inspect" or the host read capability when exact source is required for an edit or verification.`,
|
|
13167
13208
|
`Use file and directory distributions to choose evidence. Reuse the visible cursor with path or paths for match metadata; mode="summary" pages the remaining file statistics. Match counts are not relevance scores.`,
|
|
@@ -13170,7 +13211,7 @@ function siftLightPromptGuidelines(structuredOutput = true) {
|
|
|
13170
13211
|
`Use anyOf:["term1","term2"] when every exact occurrence of 2-64 literals is needed in one version-bound result. It is case-sensitive, reports anonymized condition counts, and runs requests above eight terms as bounded parallel chunks. Large condition inventories have separate continuation pages; copy those requests to retrieve the complete counts.`,
|
|
13171
13212
|
`For a changed-code question, add changes:{base:"HEAD",scope:"lines",side:"new"}; omit target for the working tree, use side:"old" for deleted-side statistics. Copy returned continuation requests to preserve source versions.`,
|
|
13172
13213
|
`Use mode:"capabilities" when the language or requested operation is unclear to get a compact lazy inventory. Use mode:"outline" with a concrete source file path for symbol counts and locations, mode:"imports" for static relationships, and mode:"tests" for related-test candidates. Their text pages summarize metadata; structured details can retain source evidence.`,
|
|
13173
|
-
`Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; business concepts
|
|
13214
|
+
`Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; use hybrid/concept only for business concepts that cannot be located by a literal clue. Use wholeWord:true for a single-pattern whole-word search. exclude contains file globs, not content negation.`,
|
|
13174
13215
|
`Use mode:"structure" only for JS/TS/TSX/Go, with a required nonempty ast-grep pattern such as "compare($X, $X)" or "send()" for code shapes across whitespace; the text page reports structural counts and locations; structured details can retain matched source evidence.`,
|
|
13175
13216
|
`Use mode:"concept" plus a natural-language query when names are unknown. It runs a pinned local multilingual model only after explicit installation; no search downloads weights or sends code to a remote model. Results expose candidate counts, score statistics, paths and the bounded ranked passage behind each candidate. Similarity scores identify candidates, not correctness.`,
|
|
13176
13217
|
`Use mode:"hybrid" plus query when wording may differ from the source. It reports exact and semantic counts separately, removes overlap, and retains one pageable evidence snapshot. conceptLimit changes only the semantic candidate count; retained candidates keep their bounded source passages.`,
|
|
@@ -20526,12 +20567,12 @@ function stringEnum(values, options) {
|
|
|
20526
20567
|
...options?.description ? { description: options.description } : {}
|
|
20527
20568
|
});
|
|
20528
20569
|
}
|
|
20529
|
-
var SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
|
|
20530
|
-
var SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
20570
|
+
var SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Routine searches should use exact content, filenames or applicable structural modes first. Omitted mode is ordinary exact search and does not load the embedding model; concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model and may take tens of seconds on an uncached scope. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
|
|
20571
|
+
var SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. Default exact search is model-free; concept/hybrid require vectorSearchEnabled:true in sift-light.json and a model. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
20531
20572
|
var siftLightSchema = _Object_({
|
|
20532
20573
|
query: Optional(String2({
|
|
20533
20574
|
maxLength: 256,
|
|
20534
|
-
description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`
|
|
20575
|
+
description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require vectorSearchEnabled:true in sift-light.json and an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`
|
|
20535
20576
|
})),
|
|
20536
20577
|
scope: Optional(stringEnum(["strict", "expand"], {
|
|
20537
20578
|
description: `${fieldGuidance("scope")}; expand (default) retries ordinary content search from project cwd. Applies to ordinary, multi-term and role searches.`
|
|
@@ -20750,25 +20791,23 @@ function modelErrorText(error) {
|
|
|
20750
20791
|
function requestContractErrorText(error) {
|
|
20751
20792
|
return requestContractProjection(error).text;
|
|
20752
20793
|
}
|
|
20753
|
-
function projectRequestContract(projected, serialized
|
|
20754
|
-
const prefix = `sift-light failed: request rejected [${projected.code}]: ${message}`;
|
|
20794
|
+
function projectRequestContract(projected, serialized) {
|
|
20755
20795
|
const recovery = projected.recovery;
|
|
20756
20796
|
const next = recovery.nextRequest ? `
|
|
20757
20797
|
Copy the nested recovery.nextRequest object unchanged; do not repeat the original query.` : `
|
|
20758
|
-
Recovery action: ${recovery.action}
|
|
20798
|
+
Recovery action: ${recovery.action}.`;
|
|
20759
20799
|
return `
|
|
20760
|
-
|
|
20761
|
-
${
|
|
20800
|
+
sift-light failed: request rejected [${projected.code}].
|
|
20801
|
+
Error details: ${serialized}${next}`;
|
|
20762
20802
|
}
|
|
20763
20803
|
function requestContractProjection(error) {
|
|
20764
20804
|
const details = boundedRequestContractDetails(error.details);
|
|
20765
20805
|
const serializedDetails = JSON.stringify(details);
|
|
20766
|
-
const
|
|
20767
|
-
const result = projectRequestContract(details, serializedDetails, boundedMessage);
|
|
20806
|
+
const result = projectRequestContract(details, serializedDetails);
|
|
20768
20807
|
if (Buffer.byteLength(result) <= MAX_REQUEST_RECOVERY_BYTES)
|
|
20769
20808
|
return { details, text: result };
|
|
20770
20809
|
const compact = {
|
|
20771
|
-
code:
|
|
20810
|
+
code: details.code,
|
|
20772
20811
|
...details.mode ? { mode: details.mode } : {},
|
|
20773
20812
|
issues: [
|
|
20774
20813
|
{
|
|
@@ -20781,7 +20820,7 @@ function requestContractProjection(error) {
|
|
|
20781
20820
|
reason: "Exact recovery was omitted; correct the request explicitly."
|
|
20782
20821
|
}
|
|
20783
20822
|
};
|
|
20784
|
-
const compactText = projectRequestContract(compact, JSON.stringify(compact)
|
|
20823
|
+
const compactText = projectRequestContract(compact, JSON.stringify(compact));
|
|
20785
20824
|
return { details: compact, text: compactText };
|
|
20786
20825
|
}
|
|
20787
20826
|
|
|
@@ -20839,10 +20878,11 @@ async function registerOmpSiftLightExtension(pi, searchPolicyAssets = new URL(".
|
|
|
20839
20878
|
const resolvedConfig = config ?? await readSiftLightConfigFile(resolveSiftLightConfigPath(ompAgentDir()), {
|
|
20840
20879
|
missing: process.env[SIFT_LIGHT_CONFIG_ENV]?.trim() ? "error" : "defaults"
|
|
20841
20880
|
});
|
|
20842
|
-
const semanticJudge =
|
|
20881
|
+
const semanticJudge = createConfiguredSemanticJudgeIntegration(resolvedConfig);
|
|
20843
20882
|
const runtime = new SiftLightRuntime(new SiftLightService({
|
|
20844
20883
|
runRipgrep: createRipgrepRunner(),
|
|
20845
20884
|
structure: createCtagsStructureProvider(),
|
|
20885
|
+
vectorSearchEnabled: resolvedConfig.vectorSearchEnabled === true,
|
|
20846
20886
|
semanticJudge
|
|
20847
20887
|
}));
|
|
20848
20888
|
const { locale } = resolvedConfig;
|
|
@@ -20861,7 +20901,7 @@ async function registerOmpSiftLightExtension(pi, searchPolicyAssets = new URL(".
|
|
|
20861
20901
|
pi.registerTool({
|
|
20862
20902
|
name: SIFT_LIGHT_LABEL,
|
|
20863
20903
|
label: SIFT_LIGHT_LABEL,
|
|
20864
|
-
description: "Search
|
|
20904
|
+
description: "Search code with bounded evidence. Routine searches use pattern for fast exact content or mode=files with query for filenames; concept/hybrid require vectorSearchEnabled:true in sift-light.json.",
|
|
20865
20905
|
approval: "read",
|
|
20866
20906
|
promptSnippet: "Search file contents without flooding context",
|
|
20867
20907
|
promptGuidelines: siftLightPromptGuidelines(),
|