@kb-labs/mind-core 2.93.0 → 2.96.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/README.md +17 -179
- package/dist/index.d.ts +397 -332
- package/dist/index.js +1340 -515
- package/dist/index.js.map +1 -1
- package/dist/services-CdtgGHNc.d.ts +14 -0
- package/dist/testing.d.ts +68 -0
- package/dist/testing.js +169 -0
- package/dist/testing.js.map +1 -0
- package/package.json +13 -8
package/dist/index.js
CHANGED
|
@@ -1,579 +1,1404 @@
|
|
|
1
|
-
|
|
2
|
-
import { createHash } from 'crypto';
|
|
3
|
-
import path, { dirname } from 'path';
|
|
4
|
-
import { existsSync, promises } from 'fs';
|
|
1
|
+
import { AgentResponseSchema, resolveScope, effectiveIndexConfig, MIND_NAMESPACE_PREFIX } from '@kb-labs/mind-contracts';
|
|
5
2
|
import { readFile } from 'fs/promises';
|
|
3
|
+
import path, { join } from 'path';
|
|
4
|
+
import globby from 'globby';
|
|
5
|
+
import { statSync, openSync, readSync, closeSync } from 'fs';
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// src/mind.ts
|
|
8
8
|
|
|
9
|
-
// src/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
// src/types.ts
|
|
10
|
+
function chunkId(path2, startLine, endLine) {
|
|
11
|
+
return `${path2}#${startLine}-${endLine}`;
|
|
12
|
+
}
|
|
13
|
+
function hashContent(text) {
|
|
14
|
+
let h = 2166136261;
|
|
15
|
+
for (let i = 0; i < text.length; i++) {
|
|
16
|
+
h ^= text.charCodeAt(i);
|
|
17
|
+
h = Math.imul(h, 16777619);
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return 3;
|
|
19
|
+
return (h >>> 0).toString(16);
|
|
20
|
+
}
|
|
21
|
+
function kindFromPath(path2) {
|
|
22
|
+
const lower = path2.toLowerCase();
|
|
23
|
+
if (lower.includes("/adr/") || /adr-\d+/.test(lower)) {
|
|
24
|
+
return "adr";
|
|
25
25
|
}
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
26
|
+
if (lower.endsWith(".md") || lower.endsWith(".mdx") || lower.endsWith(".txt")) {
|
|
27
|
+
return "doc";
|
|
28
28
|
}
|
|
29
|
-
if (
|
|
30
|
-
return
|
|
29
|
+
if (lower.endsWith(".json") || lower.endsWith(".yaml") || lower.endsWith(".yml") || lower.endsWith(".toml")) {
|
|
30
|
+
return "config";
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
return "code";
|
|
33
|
+
}
|
|
34
|
+
var IGNORE = [
|
|
35
|
+
"**/node_modules/**",
|
|
36
|
+
"**/dist/**",
|
|
37
|
+
"**/build/**",
|
|
38
|
+
"**/out/**",
|
|
39
|
+
"**/coverage/**",
|
|
40
|
+
"**/.git/**",
|
|
41
|
+
"**/.kb/**",
|
|
42
|
+
"**/bin/**",
|
|
43
|
+
"**/obj/**",
|
|
44
|
+
"**/.next/**",
|
|
45
|
+
"**/.nuxt/**",
|
|
46
|
+
"**/vendor/**",
|
|
47
|
+
"**/__pycache__/**"
|
|
48
|
+
];
|
|
49
|
+
var DENY_EXT = /* @__PURE__ */ new Set([
|
|
50
|
+
"png",
|
|
51
|
+
"jpg",
|
|
52
|
+
"jpeg",
|
|
53
|
+
"gif",
|
|
54
|
+
"svg",
|
|
55
|
+
"ico",
|
|
56
|
+
"webp",
|
|
57
|
+
"bmp",
|
|
58
|
+
"tif",
|
|
59
|
+
"tiff",
|
|
60
|
+
"avif",
|
|
61
|
+
"mp4",
|
|
62
|
+
"mov",
|
|
63
|
+
"avi",
|
|
64
|
+
"webm",
|
|
65
|
+
"mp3",
|
|
66
|
+
"wav",
|
|
67
|
+
"ogg",
|
|
68
|
+
"flac",
|
|
69
|
+
"pdf",
|
|
70
|
+
"woff",
|
|
71
|
+
"woff2",
|
|
72
|
+
"ttf",
|
|
73
|
+
"eot",
|
|
74
|
+
"otf",
|
|
75
|
+
"zip",
|
|
76
|
+
"gz",
|
|
77
|
+
"tgz",
|
|
78
|
+
"bz2",
|
|
79
|
+
"xz",
|
|
80
|
+
"tar",
|
|
81
|
+
"rar",
|
|
82
|
+
"7z",
|
|
83
|
+
"exe",
|
|
84
|
+
"dll",
|
|
85
|
+
"so",
|
|
86
|
+
"dylib",
|
|
87
|
+
"bin",
|
|
88
|
+
"wasm",
|
|
89
|
+
"class",
|
|
90
|
+
"node",
|
|
91
|
+
"pdb",
|
|
92
|
+
"xlsx",
|
|
93
|
+
"xls",
|
|
94
|
+
"docx",
|
|
95
|
+
"doc",
|
|
96
|
+
"pptx",
|
|
97
|
+
"ppt",
|
|
98
|
+
"parquet",
|
|
99
|
+
"db",
|
|
100
|
+
"sqlite",
|
|
101
|
+
"map",
|
|
102
|
+
"min.js",
|
|
103
|
+
"min.css",
|
|
104
|
+
"snap",
|
|
105
|
+
// Bulk data / logs / dumps — text, but no semantic value and they bloat the
|
|
106
|
+
// index + embedding cost (the old extension allowlist excluded these by omission).
|
|
107
|
+
"csv",
|
|
108
|
+
"tsv",
|
|
109
|
+
"ndjson",
|
|
110
|
+
"jsonl",
|
|
111
|
+
"log",
|
|
112
|
+
"sql",
|
|
113
|
+
"dump",
|
|
114
|
+
"out"
|
|
115
|
+
]);
|
|
116
|
+
var DENY_FILE = /* @__PURE__ */ new Set([
|
|
117
|
+
"package-lock.json",
|
|
118
|
+
"pnpm-lock.yaml",
|
|
119
|
+
"yarn.lock",
|
|
120
|
+
"go.sum",
|
|
121
|
+
"Cargo.lock",
|
|
122
|
+
"composer.lock"
|
|
123
|
+
]);
|
|
124
|
+
var MAX_BYTES = 512 * 1024;
|
|
125
|
+
function extOf(file) {
|
|
126
|
+
const name = file.slice(file.lastIndexOf("/") + 1).toLowerCase();
|
|
127
|
+
if (name.endsWith(".min.js")) {
|
|
128
|
+
return "min.js";
|
|
34
129
|
}
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
130
|
+
if (name.endsWith(".min.css")) {
|
|
131
|
+
return "min.css";
|
|
37
132
|
}
|
|
38
|
-
|
|
39
|
-
|
|
133
|
+
const dot = name.lastIndexOf(".");
|
|
134
|
+
return dot === -1 ? "" : name.slice(dot + 1);
|
|
135
|
+
}
|
|
136
|
+
function isBinary(abs) {
|
|
137
|
+
let fd;
|
|
138
|
+
try {
|
|
139
|
+
fd = openSync(abs, "r");
|
|
140
|
+
const buf = Buffer.alloc(4096);
|
|
141
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
142
|
+
for (let i = 0; i < n; i++) {
|
|
143
|
+
if (buf[i] === 0) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
} catch {
|
|
149
|
+
return true;
|
|
150
|
+
} finally {
|
|
151
|
+
if (fd !== void 0) {
|
|
152
|
+
closeSync(fd);
|
|
153
|
+
}
|
|
40
154
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// src/utils/token.ts
|
|
78
|
-
var DefaultTokenEstimator = class {
|
|
79
|
-
charsPerToken = 4;
|
|
80
|
-
codeBonus = 0.1;
|
|
81
|
-
// 10% bonus for code-like content
|
|
82
|
-
punctuationWeight = 0.8;
|
|
83
|
-
estimate(text) {
|
|
84
|
-
if (!text || text.length === 0) {
|
|
85
|
-
return 0;
|
|
86
|
-
}
|
|
87
|
-
const words = text.match(/\b\w+\b/g) || [];
|
|
88
|
-
const punctuation = text.match(/[^\w\s]/g) || [];
|
|
89
|
-
const whitespace = text.match(/\s/g) || [];
|
|
90
|
-
let tokens = words.length;
|
|
91
|
-
tokens += punctuation.length * this.punctuationWeight;
|
|
92
|
-
tokens += whitespace.length * 0.3;
|
|
93
|
-
const codeIndicators = text.match(/[{}();=<>]/g) || [];
|
|
94
|
-
if (codeIndicators.length > words.length * 0.1) {
|
|
95
|
-
tokens *= 1 + this.codeBonus;
|
|
96
|
-
}
|
|
97
|
-
const charBasedEstimate = text.length / this.charsPerToken;
|
|
98
|
-
return Math.ceil(Math.max(tokens, charBasedEstimate));
|
|
99
|
-
}
|
|
100
|
-
truncate(text, maxTokens, mode) {
|
|
101
|
-
if (this.estimate(text) <= maxTokens) {
|
|
102
|
-
return text;
|
|
103
|
-
}
|
|
104
|
-
const lines = text.split("\n");
|
|
105
|
-
const estimatedTokens = this.estimate(text);
|
|
106
|
-
const ratio = maxTokens / estimatedTokens;
|
|
107
|
-
const targetLines = Math.max(1, Math.floor(lines.length * ratio));
|
|
108
|
-
if (targetLines >= lines.length) {
|
|
109
|
-
return text;
|
|
110
|
-
}
|
|
111
|
-
switch (mode) {
|
|
112
|
-
case "start":
|
|
113
|
-
return lines.slice(0, targetLines).join("\n");
|
|
114
|
-
case "end":
|
|
115
|
-
return lines.slice(-targetLines).join("\n");
|
|
116
|
-
case "middle":
|
|
117
|
-
default: {
|
|
118
|
-
const startLines = Math.max(1, Math.floor(targetLines / 2));
|
|
119
|
-
const endLines = Math.max(1, targetLines - startLines);
|
|
120
|
-
const start = lines.slice(0, startLines);
|
|
121
|
-
const end = lines.slice(-endLines);
|
|
122
|
-
return [...start, "...", ...end].join("\n");
|
|
155
|
+
}
|
|
156
|
+
function trimTrailingSlashes(s) {
|
|
157
|
+
let end = s.length;
|
|
158
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47) {
|
|
159
|
+
end--;
|
|
160
|
+
}
|
|
161
|
+
return s.slice(0, end);
|
|
162
|
+
}
|
|
163
|
+
async function discover(cwd, scope) {
|
|
164
|
+
const include = scope?.include && scope.include.length > 0 ? scope.include : ["."];
|
|
165
|
+
const exclude = scope?.exclude ?? [];
|
|
166
|
+
const patterns = include.map((p) => {
|
|
167
|
+
const base = p && p !== "." ? trimTrailingSlashes(p) : "";
|
|
168
|
+
return base ? `${base}/**/*` : "**/*";
|
|
169
|
+
});
|
|
170
|
+
const candidates = await globby(patterns, {
|
|
171
|
+
cwd,
|
|
172
|
+
ignore: [...IGNORE, ...exclude],
|
|
173
|
+
dot: false,
|
|
174
|
+
followSymbolicLinks: false,
|
|
175
|
+
gitignore: false,
|
|
176
|
+
onlyFiles: true
|
|
177
|
+
});
|
|
178
|
+
return candidates.filter((rel) => {
|
|
179
|
+
if (DENY_FILE.has(rel.slice(rel.lastIndexOf("/") + 1))) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (DENY_EXT.has(extOf(rel))) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const abs = path.join(cwd, rel);
|
|
186
|
+
try {
|
|
187
|
+
const st = statSync(abs);
|
|
188
|
+
if (st.size === 0 || st.size > MAX_BYTES) {
|
|
189
|
+
return false;
|
|
123
190
|
}
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
124
193
|
}
|
|
194
|
+
return !isBinary(abs);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/ingest/chunk.ts
|
|
199
|
+
function approxTokens(line) {
|
|
200
|
+
const t = line.trim();
|
|
201
|
+
if (t === "") {
|
|
202
|
+
return 0;
|
|
125
203
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
204
|
+
return t.split(/\s+/).length;
|
|
205
|
+
}
|
|
206
|
+
function slidingWindowChunks(path2, content, opts) {
|
|
207
|
+
const lines = content.split("\n");
|
|
208
|
+
const kind = kindFromPath(path2);
|
|
209
|
+
const chunks = [];
|
|
210
|
+
const maxTokens = Math.max(1, opts.maxTokens);
|
|
211
|
+
const overlapTokens = Math.max(0, Math.min(opts.overlapTokens, maxTokens - 1));
|
|
212
|
+
let startIdx = 0;
|
|
213
|
+
while (startIdx < lines.length) {
|
|
214
|
+
let tokens = 0;
|
|
215
|
+
let endIdx = startIdx;
|
|
216
|
+
while (endIdx < lines.length && tokens < maxTokens) {
|
|
217
|
+
tokens += approxTokens(lines[endIdx] ?? "");
|
|
218
|
+
endIdx++;
|
|
219
|
+
}
|
|
220
|
+
const startLine = startIdx + 1;
|
|
221
|
+
const endLine = endIdx;
|
|
222
|
+
const text = lines.slice(startIdx, endIdx).join("\n");
|
|
223
|
+
if (text.trim() !== "") {
|
|
224
|
+
chunks.push({ id: chunkId(path2, startLine, endLine), path: path2, startLine, endLine, text, kind });
|
|
225
|
+
}
|
|
226
|
+
if (endIdx >= lines.length) {
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
let overlap = 0;
|
|
230
|
+
let stepBack = 0;
|
|
231
|
+
let i = endIdx - 1;
|
|
232
|
+
while (i > startIdx && overlap < overlapTokens) {
|
|
233
|
+
overlap += approxTokens(lines[i] ?? "");
|
|
234
|
+
stepBack++;
|
|
235
|
+
i--;
|
|
236
|
+
}
|
|
237
|
+
startIdx = Math.max(startIdx + 1, endIdx - stepBack);
|
|
238
|
+
}
|
|
239
|
+
return chunks;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/ingest/structural.ts
|
|
243
|
+
var DECL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
244
|
+
"function",
|
|
245
|
+
"class",
|
|
246
|
+
"interface",
|
|
247
|
+
"type",
|
|
248
|
+
"enum",
|
|
249
|
+
"const",
|
|
250
|
+
"let",
|
|
251
|
+
"var",
|
|
252
|
+
"namespace",
|
|
253
|
+
"module",
|
|
254
|
+
"def",
|
|
255
|
+
"func",
|
|
256
|
+
"fn",
|
|
257
|
+
"impl",
|
|
258
|
+
"struct",
|
|
259
|
+
"trait",
|
|
260
|
+
"public",
|
|
261
|
+
"private"
|
|
262
|
+
]);
|
|
263
|
+
var MODIFIERS = /* @__PURE__ */ new Set([
|
|
264
|
+
"export",
|
|
265
|
+
"default",
|
|
266
|
+
"declare",
|
|
267
|
+
"public",
|
|
268
|
+
"private",
|
|
269
|
+
"protected",
|
|
270
|
+
"static",
|
|
271
|
+
"abstract",
|
|
272
|
+
"async"
|
|
273
|
+
]);
|
|
274
|
+
function isBoundaryLine(line) {
|
|
275
|
+
if (line.length === 0 || line.charCodeAt(0) === 32 || line.charCodeAt(0) === 9) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
for (const word of line.split(/\s+/)) {
|
|
279
|
+
if (DECL_KEYWORDS.has(word)) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (!MODIFIERS.has(word)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
function approxTokens2(text) {
|
|
289
|
+
const t = text.trim();
|
|
290
|
+
return t === "" ? 0 : t.split(/\s+/).length;
|
|
291
|
+
}
|
|
292
|
+
function structuralChunks(path2, content, opts) {
|
|
293
|
+
const lines = content.split("\n");
|
|
294
|
+
const kind = kindFromPath(path2);
|
|
295
|
+
const boundaries = [];
|
|
296
|
+
lines.forEach((line, i) => {
|
|
297
|
+
if (isBoundaryLine(line)) {
|
|
298
|
+
boundaries.push(i);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
if (boundaries.length === 0) {
|
|
302
|
+
return slidingWindowChunks(path2, content, opts);
|
|
303
|
+
}
|
|
304
|
+
const starts = [.../* @__PURE__ */ new Set([0, ...boundaries])].sort((a, b) => a - b);
|
|
305
|
+
const chunks = [];
|
|
306
|
+
for (let b = 0; b < starts.length; b++) {
|
|
307
|
+
const from = starts[b];
|
|
308
|
+
const to = b + 1 < starts.length ? starts[b + 1] : lines.length;
|
|
309
|
+
const blockLines = lines.slice(from, to);
|
|
310
|
+
const blockText = blockLines.join("\n");
|
|
311
|
+
if (blockText.trim() === "") {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (approxTokens2(blockText) > opts.maxTokens) {
|
|
315
|
+
for (const sub of slidingWindowChunks(path2, blockText, opts)) {
|
|
316
|
+
const startLine = from + sub.startLine;
|
|
317
|
+
const endLine = from + sub.endLine;
|
|
318
|
+
chunks.push({ id: chunkId(path2, startLine, endLine), path: path2, startLine, endLine, text: sub.text, kind });
|
|
167
319
|
}
|
|
320
|
+
} else {
|
|
321
|
+
const startLine = from + 1;
|
|
322
|
+
const endLine = to;
|
|
323
|
+
chunks.push({ id: chunkId(path2, startLine, endLine), path: path2, startLine, endLine, text: blockText, kind });
|
|
168
324
|
}
|
|
169
|
-
if (existsSync(path.join(current, "pnpm-workspace.yaml"))) {
|
|
170
|
-
return toPosix(current);
|
|
171
|
-
}
|
|
172
|
-
current = path.dirname(current);
|
|
173
|
-
}
|
|
174
|
-
return toPosix(cwd);
|
|
175
|
-
}
|
|
176
|
-
function makeRelativeToRoot(absolutePath, root) {
|
|
177
|
-
const relative = path.relative(root, absolutePath);
|
|
178
|
-
return toPosix(relative);
|
|
179
|
-
}
|
|
180
|
-
function shouldIgnorePath(filePath) {
|
|
181
|
-
const posixPath = toPosix(filePath);
|
|
182
|
-
const ignorePatterns = [
|
|
183
|
-
"node_modules/**",
|
|
184
|
-
".git/**",
|
|
185
|
-
".kb/**",
|
|
186
|
-
// except .kb/mind/**
|
|
187
|
-
"dist/**",
|
|
188
|
-
"coverage/**",
|
|
189
|
-
".turbo/**",
|
|
190
|
-
".vite/**",
|
|
191
|
-
"**/*.log",
|
|
192
|
-
"**/*.tmp",
|
|
193
|
-
"**/*.temp"
|
|
194
|
-
];
|
|
195
|
-
const extensionPatterns = [".log", ".tmp", ".temp"];
|
|
196
|
-
if (posixPath.startsWith(".kb/") && !posixPath.startsWith(".kb/mind/")) {
|
|
197
|
-
return true;
|
|
198
325
|
}
|
|
199
|
-
|
|
326
|
+
return chunks;
|
|
327
|
+
}
|
|
328
|
+
function chunkFile(path2, content, opts, ast) {
|
|
329
|
+
if (ast && kindFromPath(path2) === "code") {
|
|
330
|
+
return structuralChunks(path2, content, opts);
|
|
331
|
+
}
|
|
332
|
+
return slidingWindowChunks(path2, content, opts);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/ingest/embed.ts
|
|
336
|
+
var MAX_BATCH_CHUNKS = 96;
|
|
337
|
+
var MAX_BATCH_TOKENS = 2e5;
|
|
338
|
+
var MAX_INPUT_CHARS = 12e3;
|
|
339
|
+
var CHARS_PER_TOKEN = 4;
|
|
340
|
+
var approxTokens3 = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
341
|
+
function toRecord(chunk, vector, text) {
|
|
342
|
+
const meta = {
|
|
343
|
+
path: chunk.path,
|
|
344
|
+
startLine: chunk.startLine,
|
|
345
|
+
endLine: chunk.endLine,
|
|
346
|
+
text,
|
|
347
|
+
kind: chunk.kind
|
|
348
|
+
};
|
|
349
|
+
return { id: chunk.id, vector, metadata: meta };
|
|
350
|
+
}
|
|
351
|
+
function planBatches(chunks) {
|
|
352
|
+
const items = chunks.map((c) => ({
|
|
353
|
+
chunk: c,
|
|
354
|
+
text: c.text.length > MAX_INPUT_CHARS ? c.text.slice(0, MAX_INPUT_CHARS) : c.text
|
|
355
|
+
}));
|
|
356
|
+
const batches = [];
|
|
357
|
+
let cur = [];
|
|
358
|
+
let curTokens = 0;
|
|
359
|
+
for (const item of items) {
|
|
360
|
+
const t = approxTokens3(item.text);
|
|
361
|
+
if (cur.length > 0 && (cur.length >= MAX_BATCH_CHUNKS || curTokens + t > MAX_BATCH_TOKENS)) {
|
|
362
|
+
batches.push(cur);
|
|
363
|
+
cur = [];
|
|
364
|
+
curTokens = 0;
|
|
365
|
+
}
|
|
366
|
+
cur.push(item);
|
|
367
|
+
curTokens += t;
|
|
368
|
+
}
|
|
369
|
+
if (cur.length > 0) {
|
|
370
|
+
batches.push(cur);
|
|
371
|
+
}
|
|
372
|
+
return batches;
|
|
373
|
+
}
|
|
374
|
+
async function embedChunks(chunks, embeddings, onProgress) {
|
|
375
|
+
if (chunks.length === 0) {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
const records = [];
|
|
379
|
+
for (const batch of planBatches(chunks)) {
|
|
380
|
+
const vectors = await embeddings.embedBatch(batch.map((b) => b.text));
|
|
381
|
+
batch.forEach((b, j) => records.push(toRecord(b.chunk, vectors[j] ?? [], b.chunk.text)));
|
|
382
|
+
onProgress?.(records.length, chunks.length);
|
|
383
|
+
}
|
|
384
|
+
return records;
|
|
385
|
+
}
|
|
386
|
+
function manifestPath(indexId) {
|
|
387
|
+
return `mind/${indexId}/manifest.json`;
|
|
388
|
+
}
|
|
389
|
+
async function isStale(manifest, file, cwd) {
|
|
390
|
+
const entry = manifest.files[file];
|
|
391
|
+
if (!entry) {
|
|
200
392
|
return false;
|
|
201
393
|
}
|
|
202
|
-
|
|
394
|
+
try {
|
|
395
|
+
const content = await readFile(join(cwd, file), "utf8");
|
|
396
|
+
return hashContent(content) !== entry.hash;
|
|
397
|
+
} catch {
|
|
203
398
|
return true;
|
|
204
399
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
400
|
+
}
|
|
401
|
+
async function staleMap(manifest, files, cwd) {
|
|
402
|
+
const out = /* @__PURE__ */ new Map();
|
|
403
|
+
await Promise.all(
|
|
404
|
+
[...new Set(files)].map(async (f) => {
|
|
405
|
+
out.set(f, await isStale(manifest, f, cwd));
|
|
406
|
+
})
|
|
407
|
+
);
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
async function staleCount(manifest, cwd) {
|
|
411
|
+
const flags = await staleMap(manifest, Object.keys(manifest.files), cwd);
|
|
412
|
+
return [...flags.values()].filter(Boolean).length;
|
|
413
|
+
}
|
|
414
|
+
async function manifestExists(storage, indexId) {
|
|
415
|
+
return Boolean(await storage.read(manifestPath(indexId)));
|
|
416
|
+
}
|
|
417
|
+
async function loadManifest(storage, indexId) {
|
|
418
|
+
const buf = await storage.read(manifestPath(indexId));
|
|
419
|
+
if (!buf) {
|
|
420
|
+
return { indexId, chunks: [], files: {}, updatedAt: null };
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const parsed = JSON.parse(buf.toString("utf8"));
|
|
424
|
+
return { indexId, chunks: parsed.chunks ?? [], files: parsed.files ?? {}, updatedAt: parsed.updatedAt ?? null };
|
|
425
|
+
} catch {
|
|
426
|
+
return { indexId, chunks: [], files: {}, updatedAt: null };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function saveManifest(storage, manifest) {
|
|
430
|
+
const buf = Buffer.from(JSON.stringify(manifest), "utf8");
|
|
431
|
+
await storage.write(manifestPath(manifest.indexId), buf);
|
|
432
|
+
}
|
|
433
|
+
async function deleteManifest(storage, indexId) {
|
|
434
|
+
await storage.delete(manifestPath(indexId));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/ingest/ingest.ts
|
|
438
|
+
async function readAndHash(cwd, paths, logger) {
|
|
439
|
+
const contentByPath = /* @__PURE__ */ new Map();
|
|
440
|
+
const hashByPath = /* @__PURE__ */ new Map();
|
|
441
|
+
for (const path2 of paths) {
|
|
442
|
+
let content;
|
|
443
|
+
try {
|
|
444
|
+
content = await readFile(join(cwd, path2), "utf8");
|
|
445
|
+
} catch (err) {
|
|
446
|
+
logger?.warn("mind: skipped unreadable file", { path: path2, reason: err instanceof Error ? err.message : String(err) });
|
|
447
|
+
continue;
|
|
209
448
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
449
|
+
contentByPath.set(path2, content);
|
|
450
|
+
hashByPath.set(path2, hashContent(content));
|
|
451
|
+
}
|
|
452
|
+
return { contentByPath, hashByPath };
|
|
453
|
+
}
|
|
454
|
+
function classify(contentByPath, hashByPath, prevFiles) {
|
|
455
|
+
const toIndex = [];
|
|
456
|
+
const unchanged = [];
|
|
457
|
+
let added = 0;
|
|
458
|
+
let updated = 0;
|
|
459
|
+
for (const path2 of contentByPath.keys()) {
|
|
460
|
+
const prevEntry = prevFiles[path2];
|
|
461
|
+
if (prevEntry && prevEntry.hash === hashByPath.get(path2)) {
|
|
462
|
+
unchanged.push(path2);
|
|
463
|
+
} else {
|
|
464
|
+
toIndex.push(path2);
|
|
465
|
+
if (prevEntry) {
|
|
466
|
+
updated++;
|
|
467
|
+
} else {
|
|
468
|
+
added++;
|
|
469
|
+
}
|
|
213
470
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
471
|
+
}
|
|
472
|
+
const removedPaths = Object.keys(prevFiles).filter((p) => !contentByPath.has(p));
|
|
473
|
+
return { toIndex, unchanged, removedPaths, added, updated };
|
|
474
|
+
}
|
|
475
|
+
async function ingest(input, services) {
|
|
476
|
+
const { storage, embeddings, vectorStore } = services;
|
|
477
|
+
const prev = await loadManifest(storage, input.indexId);
|
|
478
|
+
const prevFiles = input.full ? {} : prev.files;
|
|
479
|
+
const emit = input.onProgress ?? (() => {
|
|
480
|
+
});
|
|
481
|
+
const paths = await discover(input.cwd, input.scope);
|
|
482
|
+
emit({ stage: "discover", files: paths.length });
|
|
483
|
+
const { contentByPath, hashByPath } = await readAndHash(input.cwd, paths, services.logger);
|
|
484
|
+
const { toIndex, unchanged, removedPaths, added, updated } = classify(contentByPath, hashByPath, prevFiles);
|
|
485
|
+
emit({ stage: "delta", toIndex: toIndex.length, unchanged: unchanged.length, removed: removedPaths.length });
|
|
486
|
+
const stalePaths = new Set(input.full ? Object.keys(prev.files) : [...toIndex, ...removedPaths]);
|
|
487
|
+
const staleIds = (input.full ? prev.chunks : prev.chunks.filter((c) => stalePaths.has(c.path))).map((c) => c.id);
|
|
488
|
+
if (staleIds.length > 0) {
|
|
489
|
+
await vectorStore.delete(staleIds, input.indexId);
|
|
490
|
+
}
|
|
491
|
+
const unchangedSet = new Set(unchanged);
|
|
492
|
+
const keptChunks = input.full ? [] : prev.chunks.filter((c) => unchangedSet.has(c.path));
|
|
493
|
+
const files = {};
|
|
494
|
+
for (const path2 of unchanged) {
|
|
495
|
+
files[path2] = prevFiles[path2];
|
|
496
|
+
}
|
|
497
|
+
const newChunks = [];
|
|
498
|
+
for (const path2 of toIndex) {
|
|
499
|
+
const chunks = chunkFile(path2, contentByPath.get(path2), input.chunk, input.ast);
|
|
500
|
+
if (chunks.length === 0) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
newChunks.push(...chunks);
|
|
504
|
+
files[path2] = { chunks: chunks.length, indexedAt: input.now, hash: hashByPath.get(path2) };
|
|
505
|
+
}
|
|
506
|
+
emit({ stage: "chunk", chunks: newChunks.length });
|
|
507
|
+
const records = await embedChunks(
|
|
508
|
+
newChunks,
|
|
509
|
+
embeddings,
|
|
510
|
+
(done, total) => emit({ stage: "embed", done, total })
|
|
511
|
+
);
|
|
512
|
+
if (records.length > 0) {
|
|
513
|
+
emit({ stage: "upsert", count: records.length });
|
|
514
|
+
await vectorStore.upsert(records, input.indexId);
|
|
515
|
+
}
|
|
516
|
+
emit({ stage: "save" });
|
|
517
|
+
const manifest = {
|
|
518
|
+
indexId: input.indexId,
|
|
519
|
+
chunks: [...keptChunks, ...newChunks],
|
|
520
|
+
files,
|
|
521
|
+
updatedAt: input.now
|
|
522
|
+
};
|
|
523
|
+
await saveManifest(storage, manifest);
|
|
524
|
+
return {
|
|
525
|
+
filesIndexed: Object.keys(files).length,
|
|
526
|
+
chunks: manifest.chunks.length,
|
|
527
|
+
added,
|
|
528
|
+
updated,
|
|
529
|
+
removed: removedPaths.length,
|
|
530
|
+
unchanged: unchanged.length
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
async function removePaths(manifest, paths, services, indexId) {
|
|
534
|
+
const removedIds = manifest.chunks.filter((c) => paths.has(c.path)).map((c) => c.id);
|
|
535
|
+
if (removedIds.length > 0) {
|
|
536
|
+
await services.vectorStore.delete(removedIds, indexId);
|
|
537
|
+
}
|
|
538
|
+
manifest.chunks = manifest.chunks.filter((c) => !paths.has(c.path));
|
|
539
|
+
for (const p of paths) {
|
|
540
|
+
delete manifest.files[p];
|
|
541
|
+
}
|
|
542
|
+
return removedIds.length;
|
|
543
|
+
}
|
|
544
|
+
async function addPaths(manifest, paths, services, opts) {
|
|
545
|
+
const newChunks = [];
|
|
546
|
+
for (const path2 of paths) {
|
|
547
|
+
let content;
|
|
548
|
+
try {
|
|
549
|
+
content = await readFile(join(opts.cwd, path2), "utf8");
|
|
550
|
+
} catch {
|
|
551
|
+
continue;
|
|
217
552
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
553
|
+
const chunks = chunkFile(path2, content, opts.chunk, opts.ast);
|
|
554
|
+
if (chunks.length === 0) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
newChunks.push(...chunks);
|
|
558
|
+
manifest.files[path2] = { chunks: chunks.length, indexedAt: opts.now, hash: hashContent(content) };
|
|
559
|
+
}
|
|
560
|
+
if (newChunks.length > 0) {
|
|
561
|
+
const records = await embedChunks(newChunks, services.embeddings);
|
|
562
|
+
await services.vectorStore.upsert(records, opts.indexId);
|
|
563
|
+
manifest.chunks.push(...newChunks);
|
|
564
|
+
}
|
|
565
|
+
return newChunks.length;
|
|
566
|
+
}
|
|
567
|
+
async function syncAdd(paths, services, opts) {
|
|
568
|
+
const manifest = await loadManifest(services.storage, opts.indexId);
|
|
569
|
+
const existing = new Set(paths.filter((p) => manifest.files[p]));
|
|
570
|
+
await removePaths(manifest, existing, services, opts.indexId);
|
|
571
|
+
await addPaths(manifest, paths, services, opts);
|
|
572
|
+
manifest.updatedAt = opts.now;
|
|
573
|
+
await saveManifest(services.storage, manifest);
|
|
574
|
+
return { added: paths.length - existing.size, updated: existing.size, deleted: 0 };
|
|
575
|
+
}
|
|
576
|
+
async function syncUpdate(paths, services, opts) {
|
|
577
|
+
const manifest = await loadManifest(services.storage, opts.indexId);
|
|
578
|
+
await removePaths(manifest, new Set(paths), services, opts.indexId);
|
|
579
|
+
await addPaths(manifest, paths, services, opts);
|
|
580
|
+
manifest.updatedAt = opts.now;
|
|
581
|
+
await saveManifest(services.storage, manifest);
|
|
582
|
+
return { added: 0, updated: paths.length, deleted: 0 };
|
|
583
|
+
}
|
|
584
|
+
async function syncDelete(paths, services, opts) {
|
|
585
|
+
const manifest = await loadManifest(services.storage, opts.indexId);
|
|
586
|
+
const deleted = await removePaths(manifest, new Set(paths), services, opts.indexId);
|
|
587
|
+
manifest.updatedAt = opts.now;
|
|
588
|
+
await saveManifest(services.storage, manifest);
|
|
589
|
+
return { added: 0, updated: 0, deleted };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/retrieval/bm25.ts
|
|
593
|
+
var K1 = 1.5;
|
|
594
|
+
var B = 0.75;
|
|
595
|
+
function tokenize(text) {
|
|
596
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
|
|
597
|
+
}
|
|
598
|
+
function bm25Search(chunks, query, limit) {
|
|
599
|
+
if (chunks.length === 0) {
|
|
600
|
+
return [];
|
|
601
|
+
}
|
|
602
|
+
const docs = chunks.map((c) => tokenize(c.text));
|
|
603
|
+
const docLens = docs.map((d) => d.length);
|
|
604
|
+
const avgDocLen = docLens.reduce((a, b) => a + b, 0) / docs.length || 1;
|
|
605
|
+
const df = /* @__PURE__ */ new Map();
|
|
606
|
+
for (const doc of docs) {
|
|
607
|
+
for (const term of new Set(doc)) {
|
|
608
|
+
df.set(term, (df.get(term) ?? 0) + 1);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const queryTerms = [...new Set(tokenize(query))];
|
|
612
|
+
const N = docs.length;
|
|
613
|
+
const scored = chunks.map((chunk, i) => {
|
|
614
|
+
const doc = docs[i] ?? [];
|
|
615
|
+
const len = docLens[i] ?? 0;
|
|
616
|
+
const tf = /* @__PURE__ */ new Map();
|
|
617
|
+
for (const term of doc) {
|
|
618
|
+
tf.set(term, (tf.get(term) ?? 0) + 1);
|
|
619
|
+
}
|
|
620
|
+
let score = 0;
|
|
621
|
+
for (const term of queryTerms) {
|
|
622
|
+
const termTf = tf.get(term);
|
|
623
|
+
if (!termTf) {
|
|
624
|
+
continue;
|
|
222
625
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
626
|
+
const docFreq = df.get(term) ?? 0;
|
|
627
|
+
const idf = Math.log(1 + (N - docFreq + 0.5) / (docFreq + 0.5));
|
|
628
|
+
const denom = termTf + K1 * (1 - B + B * len / avgDocLen);
|
|
629
|
+
score += idf * (termTf * (K1 + 1) / denom);
|
|
225
630
|
}
|
|
226
|
-
return
|
|
631
|
+
return { id: chunk.id, score };
|
|
227
632
|
});
|
|
633
|
+
return scored.filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
228
634
|
}
|
|
229
635
|
|
|
230
|
-
// src/
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
636
|
+
// src/retrieval/vector.ts
|
|
637
|
+
async function vectorSearch(queryText, vectorStore, embed, indexId, limit) {
|
|
638
|
+
let vector;
|
|
639
|
+
try {
|
|
640
|
+
vector = await embed(queryText);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
throw new Error(`mind: query embedding failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
234
643
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
644
|
+
try {
|
|
645
|
+
const hits = await vectorStore.search(vector, limit, void 0, indexId);
|
|
646
|
+
return hits.map((h) => ({ id: h.id, score: h.score }));
|
|
647
|
+
} catch (err) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
`mind: vector search failed (indexId="${indexId}", dims=${vector.length}) \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/retrieval/fuse.ts
|
|
655
|
+
function intentWeights(intent) {
|
|
656
|
+
switch (intent) {
|
|
657
|
+
case "lookup":
|
|
658
|
+
return { vector: 0.4, bm25: 0.6 };
|
|
659
|
+
// exact symbol lookups lean keyword
|
|
660
|
+
case "architecture":
|
|
661
|
+
return { vector: 0.6, bm25: 0.4 };
|
|
662
|
+
case "concept":
|
|
663
|
+
default:
|
|
664
|
+
return { vector: 0.7, bm25: 0.3 };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function rrfFuse(lists, k) {
|
|
668
|
+
const scores = /* @__PURE__ */ new Map();
|
|
669
|
+
const labels = /* @__PURE__ */ new Map();
|
|
670
|
+
for (const { ranked, weight, label } of lists) {
|
|
671
|
+
ranked.forEach((item, idx) => {
|
|
672
|
+
const rank = idx + 1;
|
|
673
|
+
scores.set(item.id, (scores.get(item.id) ?? 0) + weight * (1 / (k + rank)));
|
|
674
|
+
if (label) {
|
|
675
|
+
const set = labels.get(item.id) ?? /* @__PURE__ */ new Set();
|
|
676
|
+
set.add(label);
|
|
677
|
+
labels.set(item.id, set);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
return [...scores.entries()].map(([id, score]) => ({ id, score, matchedBy: resolveMatchedBy(labels.get(id)) })).sort((a, b) => b.score - a.score);
|
|
682
|
+
}
|
|
683
|
+
function resolveMatchedBy(set) {
|
|
684
|
+
if (!set || set.size === 0 || set.size === 2) {
|
|
685
|
+
return "both";
|
|
686
|
+
}
|
|
687
|
+
return set.has("semantic") ? "semantic" : "lexical";
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/retrieval/hyde.ts
|
|
691
|
+
var HYDE_PROMPT = (query) => `Write a short, realistic code or documentation snippet that would directly answer the question below. Output only the snippet \u2014 no preamble, no explanation, no fences.
|
|
692
|
+
|
|
693
|
+
Question: ${query}`;
|
|
694
|
+
async function hypotheticalDocument(query, llm) {
|
|
695
|
+
try {
|
|
696
|
+
const { content } = await llm.complete(HYDE_PROMPT(query), { temperature: 0, maxTokens: 256 });
|
|
697
|
+
const text = (content ?? "").trim();
|
|
698
|
+
return text === "" ? query : `${text}
|
|
699
|
+
|
|
700
|
+
${query}`;
|
|
701
|
+
} catch {
|
|
702
|
+
return query;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/retrieval/expand.ts
|
|
707
|
+
var EXPAND_PROMPT = (query) => `List code identifiers, function/type names, and close synonyms that would likely appear in source code or documentation relevant to the query below. Output a single space-separated line of terms only \u2014 no explanation, no punctuation, no fences.
|
|
708
|
+
|
|
709
|
+
Query: ${query}`;
|
|
710
|
+
async function expandQuery(query, llm) {
|
|
711
|
+
try {
|
|
712
|
+
const { content } = await llm.complete(EXPAND_PROMPT(query), { temperature: 0, maxTokens: 64 });
|
|
713
|
+
const extra = (content ?? "").trim();
|
|
714
|
+
return extra === "" ? query : `${query} ${extra}`;
|
|
715
|
+
} catch {
|
|
716
|
+
return query;
|
|
247
717
|
}
|
|
248
|
-
return dotProduct2 / Math.sqrt(normA * normB);
|
|
249
718
|
}
|
|
250
|
-
|
|
251
|
-
|
|
719
|
+
|
|
720
|
+
// src/retrieval/retrieve.ts
|
|
721
|
+
async function retrieve(input, services) {
|
|
722
|
+
const manifest = await loadManifest(services.storage, input.indexId);
|
|
723
|
+
const byId = new Map(manifest.chunks.map((c) => [c.id, c]));
|
|
724
|
+
const candidateLimit = Math.max(input.limit * 3, 30);
|
|
725
|
+
const vectorText = input.hyde ? await hypotheticalDocument(input.text, services.llm) : input.text;
|
|
726
|
+
const lexicalText = input.expand ? await expandQuery(input.text, services.llm) : input.text;
|
|
727
|
+
const bm25 = bm25Search(manifest.chunks, lexicalText, candidateLimit);
|
|
728
|
+
const vector = await vectorSearch(
|
|
729
|
+
vectorText,
|
|
730
|
+
services.vectorStore,
|
|
731
|
+
(t) => services.embeddings.embed(t),
|
|
732
|
+
input.indexId,
|
|
733
|
+
candidateLimit
|
|
734
|
+
);
|
|
735
|
+
const weights = intentWeights(input.intent);
|
|
736
|
+
const fused = rrfFuse(
|
|
737
|
+
[
|
|
738
|
+
{ ranked: vector, weight: weights.vector, label: "semantic" },
|
|
739
|
+
{ ranked: bm25, weight: weights.bm25, label: "lexical" }
|
|
740
|
+
],
|
|
741
|
+
input.rrfK
|
|
742
|
+
);
|
|
743
|
+
const ranked = fused.map((r) => {
|
|
744
|
+
const chunk = byId.get(r.id);
|
|
745
|
+
return chunk ? { chunk, score: r.score, matchedBy: r.matchedBy } : void 0;
|
|
746
|
+
}).filter((r) => r !== void 0).slice(0, input.limit);
|
|
747
|
+
const semanticWinRate = ranked.length === 0 ? 0 : ranked.filter((r) => r.matchedBy === "semantic").length / ranked.length;
|
|
748
|
+
return { ranked, confidence: confidenceFrom(vector), semanticWinRate };
|
|
749
|
+
}
|
|
750
|
+
function confidenceFrom(vector) {
|
|
751
|
+
if (vector.length === 0) {
|
|
252
752
|
return 0;
|
|
253
753
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
return vec.map((v) => (v ?? 0) / mag);
|
|
274
|
-
}
|
|
275
|
-
var FileRotationStore = class {
|
|
276
|
-
constructor(storage, options = {}) {
|
|
277
|
-
this.storage = storage;
|
|
278
|
-
this.basePath = options.basePath ? this.ensureTrailingSlash(options.basePath) : ".kb/mind/store/";
|
|
279
|
-
this.filePrefix = options.filePrefix ?? "store-";
|
|
280
|
-
this.maxRecordsPerFile = options.maxRecordsPerFile ?? 1e3;
|
|
281
|
-
this.maxFiles = options.maxFiles ?? 30;
|
|
282
|
-
}
|
|
283
|
-
storage;
|
|
284
|
-
basePath;
|
|
285
|
-
filePrefix;
|
|
286
|
-
maxRecordsPerFile;
|
|
287
|
-
maxFiles;
|
|
288
|
-
/**
|
|
289
|
-
* Append a record to the current writable file
|
|
290
|
-
*
|
|
291
|
-
* Automatically handles:
|
|
292
|
-
* - File rotation when maxRecordsPerFile exceeded
|
|
293
|
-
* - Cleanup when maxFiles exceeded
|
|
294
|
-
* - JSONL formatting
|
|
295
|
-
*
|
|
296
|
-
* @param record - Record to append
|
|
297
|
-
*/
|
|
298
|
-
async appendRecord(record) {
|
|
299
|
-
const target = await this.getWritableFile();
|
|
300
|
-
const line = JSON.stringify({ v: 1, record }) + "\n";
|
|
301
|
-
const existing = await this.storage.read(target);
|
|
302
|
-
const buffer = existing ? Buffer.concat([existing, Buffer.from(line, "utf8")]) : Buffer.from(line, "utf8");
|
|
303
|
-
await this.storage.write(target, buffer);
|
|
304
|
-
await this.enforceRotation();
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Read records from all files, optionally filtering
|
|
308
|
-
*
|
|
309
|
-
* @param filter - Optional filter function
|
|
310
|
-
* @param limit - Maximum number of records to return
|
|
311
|
-
* @returns Array of records matching filter
|
|
312
|
-
*/
|
|
313
|
-
async readRecords(filter, limit) {
|
|
314
|
-
const files = await this.getFilesSorted();
|
|
315
|
-
const results = [];
|
|
316
|
-
for (const file of files) {
|
|
317
|
-
if (limit && results.length >= limit) {
|
|
318
|
-
break;
|
|
754
|
+
const top = vector.slice(0, 3);
|
|
755
|
+
const avg = top.reduce((a, r) => a + r.score, 0) / top.length;
|
|
756
|
+
return Math.max(0, Math.min(1, avg));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/retrieval/rerank.ts
|
|
760
|
+
function rerank(ranked, query) {
|
|
761
|
+
const qTokens = new Set(tokenize(query));
|
|
762
|
+
if (qTokens.size === 0) {
|
|
763
|
+
return ranked;
|
|
764
|
+
}
|
|
765
|
+
const boosted = ranked.map((item) => {
|
|
766
|
+
const docTokens = tokenize(item.chunk.text);
|
|
767
|
+
const docSet = new Set(docTokens);
|
|
768
|
+
let covered = 0;
|
|
769
|
+
for (const q of qTokens) {
|
|
770
|
+
if (docSet.has(q)) {
|
|
771
|
+
covered++;
|
|
319
772
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
773
|
+
}
|
|
774
|
+
const coverage = covered / qTokens.size;
|
|
775
|
+
const lowerText = item.chunk.text.toLowerCase();
|
|
776
|
+
const verbatim = [...qTokens].some((q) => q.length >= 3 && lowerText.includes(q)) ? 1 : 0;
|
|
777
|
+
const boost = 1 + 0.5 * coverage + 0.25 * verbatim;
|
|
778
|
+
return { ...item, score: item.score * boost };
|
|
779
|
+
});
|
|
780
|
+
return boosted.sort((a, b) => b.score - a.score);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/retrieval/dedup.ts
|
|
784
|
+
function jaccard(a, b) {
|
|
785
|
+
if (a.size === 0 && b.size === 0) {
|
|
786
|
+
return 1;
|
|
787
|
+
}
|
|
788
|
+
let inter = 0;
|
|
789
|
+
for (const t of a) {
|
|
790
|
+
if (b.has(t)) {
|
|
791
|
+
inter++;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const union = a.size + b.size - inter;
|
|
795
|
+
return union === 0 ? 0 : inter / union;
|
|
796
|
+
}
|
|
797
|
+
function dedupRanked(ranked, threshold = 0.85) {
|
|
798
|
+
const kept = [];
|
|
799
|
+
const keptTokens = [];
|
|
800
|
+
for (const item of ranked) {
|
|
801
|
+
const tokens = new Set(tokenize(item.chunk.text));
|
|
802
|
+
const isDup = keptTokens.some((k) => jaccard(tokens, k) >= threshold);
|
|
803
|
+
if (!isDup) {
|
|
804
|
+
kept.push(item);
|
|
805
|
+
keptTokens.push(tokens);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return kept;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/answer/verify.ts
|
|
812
|
+
var FILE_WEIGHT = 0.7;
|
|
813
|
+
var SNIPPET_WEIGHT = 0.3;
|
|
814
|
+
async function verifySources(ranked, storage) {
|
|
815
|
+
if (ranked.length === 0) {
|
|
816
|
+
return { rate: 1, perChunk: [] };
|
|
817
|
+
}
|
|
818
|
+
const perChunk = [];
|
|
819
|
+
for (const { chunk } of ranked) {
|
|
820
|
+
const exists = await storage.exists(chunk.path);
|
|
821
|
+
let score = exists ? FILE_WEIGHT : 0;
|
|
822
|
+
if (exists) {
|
|
823
|
+
const buf = await storage.read(chunk.path);
|
|
824
|
+
const content = buf?.toString("utf8") ?? "";
|
|
825
|
+
const probe = chunk.text.split("\n").find((l) => l.trim().length > 8)?.trim();
|
|
826
|
+
if (probe && content.includes(probe)) {
|
|
827
|
+
score += SNIPPET_WEIGHT;
|
|
323
828
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
829
|
+
}
|
|
830
|
+
perChunk.push(score);
|
|
831
|
+
}
|
|
832
|
+
const rate = perChunk.reduce((a, b) => a + b, 0) / perChunk.length;
|
|
833
|
+
return { rate, perChunk };
|
|
834
|
+
}
|
|
835
|
+
function computeConfidence(retrievalConfidence, verificationRate, floor) {
|
|
836
|
+
const confidence = Math.max(0, Math.min(1, retrievalConfidence * verificationRate));
|
|
837
|
+
const warnings = [];
|
|
838
|
+
if (confidence < floor) {
|
|
839
|
+
warnings.push({
|
|
840
|
+
code: "LOW_CONFIDENCE",
|
|
841
|
+
message: `Confidence ${confidence.toFixed(2)} is below the floor ${floor.toFixed(2)}; the answer may be unreliable.`
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return { confidence, warnings };
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/answer/field-check.ts
|
|
848
|
+
function isCodeSymbol(t) {
|
|
849
|
+
if (t.length < 3 || t.length > 80) {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
if (/\.(ts|tsx|js|jsx|json|md|go|py|rs)$/.test(t)) {
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
if (t.includes(".") && /[a-zA-Z]/.test(t)) {
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
if (/_/.test(t) && /[a-zA-Z]/.test(t)) {
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
if (/[a-z][A-Z]/.test(t)) {
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
function extractSymbols(answer) {
|
|
867
|
+
const out = /* @__PURE__ */ new Set();
|
|
868
|
+
for (const m of answer.matchAll(/`([^`\n]{2,80})`/g)) {
|
|
869
|
+
for (const tok of m[1].trim().split(/[\s(),;:[\]{}'"]+/)) {
|
|
870
|
+
if (isCodeSymbol(tok)) {
|
|
871
|
+
out.add(tok);
|
|
338
872
|
}
|
|
339
873
|
}
|
|
340
|
-
return limit ? results.slice(0, limit) : results;
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Get the current writable file path
|
|
344
|
-
*
|
|
345
|
-
* Creates a new segment if:
|
|
346
|
-
* - No files exist
|
|
347
|
-
* - Latest file has >= maxRecordsPerFile records
|
|
348
|
-
*
|
|
349
|
-
* @returns Path to writable file
|
|
350
|
-
*/
|
|
351
|
-
async getWritableFile() {
|
|
352
|
-
const files = await this.getFilesSorted();
|
|
353
|
-
if (files.length === 0) {
|
|
354
|
-
return this.segmentPath(Date.now());
|
|
355
|
-
}
|
|
356
|
-
const latest = files[files.length - 1];
|
|
357
|
-
const buf = await this.storage.read(latest);
|
|
358
|
-
if (!buf) {
|
|
359
|
-
return latest;
|
|
360
|
-
}
|
|
361
|
-
const count = buf.toString("utf8").split("\n").filter(Boolean).length;
|
|
362
|
-
if (count >= this.maxRecordsPerFile) {
|
|
363
|
-
return this.segmentPath(Date.now());
|
|
364
|
-
}
|
|
365
|
-
return latest;
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Get all store files sorted by timestamp (oldest to newest)
|
|
369
|
-
*
|
|
370
|
-
* @returns Sorted array of file paths
|
|
371
|
-
*/
|
|
372
|
-
async getFilesSorted() {
|
|
373
|
-
const files = await this.storage.list(this.basePath);
|
|
374
|
-
return files.filter((f) => f.startsWith(this.basePath + this.filePrefix) && f.endsWith(".jsonl")).sort();
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Generate a segment file path from timestamp
|
|
378
|
-
*
|
|
379
|
-
* Format: {filePrefix}YYYYMMDD-{timestamp}.jsonl
|
|
380
|
-
* Example: history-20251209-1733769000123.jsonl
|
|
381
|
-
*
|
|
382
|
-
* @param ts - Unix timestamp in milliseconds
|
|
383
|
-
* @returns Full file path
|
|
384
|
-
*/
|
|
385
|
-
segmentPath(ts) {
|
|
386
|
-
const date = new Date(ts);
|
|
387
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
388
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
389
|
-
const year = date.getFullYear();
|
|
390
|
-
const filename = `${this.filePrefix}${year}${month}${day}-${ts}.jsonl`;
|
|
391
|
-
return path.posix.join(this.basePath, filename);
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Enforce file rotation by deleting oldest files if maxFiles exceeded
|
|
395
|
-
*/
|
|
396
|
-
async enforceRotation() {
|
|
397
|
-
const files = await this.getFilesSorted();
|
|
398
|
-
if (files.length <= this.maxFiles) {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const excess = files.length - this.maxFiles;
|
|
402
|
-
const toDelete = files.slice(0, excess);
|
|
403
|
-
await Promise.all(toDelete.map((f) => this.storage.delete(f)));
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Ensure path ends with trailing slash
|
|
407
|
-
*/
|
|
408
|
-
ensureTrailingSlash(p) {
|
|
409
|
-
return p.endsWith("/") ? p : `${p}/`;
|
|
410
874
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
875
|
+
for (const m of answer.matchAll(/[A-Za-z_$][A-Za-z0-9_$-]*(?:[./][A-Za-z0-9_$.-]+)*/g)) {
|
|
876
|
+
if (isCodeSymbol(m[0])) {
|
|
877
|
+
out.add(m[0]);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return [...out];
|
|
881
|
+
}
|
|
882
|
+
function checkFields(answer, ranked) {
|
|
883
|
+
const symbols = extractSymbols(answer);
|
|
884
|
+
if (symbols.length === 0) {
|
|
885
|
+
return { rate: 1, ungrounded: [], checked: 0 };
|
|
415
886
|
}
|
|
416
|
-
|
|
417
|
-
|
|
887
|
+
const corpus = ranked.map((r) => `${r.chunk.path}
|
|
888
|
+
${r.chunk.text}`).join("\n").toLowerCase();
|
|
889
|
+
const grounded = (sym) => {
|
|
890
|
+
const s = sym.toLowerCase();
|
|
891
|
+
if (corpus.includes(s)) {
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
const head = s.split(".")[0];
|
|
895
|
+
return head.length >= 3 && corpus.includes(head);
|
|
896
|
+
};
|
|
897
|
+
const ungrounded = symbols.filter((s) => !grounded(s));
|
|
898
|
+
return { rate: (symbols.length - ungrounded.length) / symbols.length, ungrounded, checked: symbols.length };
|
|
899
|
+
}
|
|
900
|
+
function applyFieldCheck(answer, ranked, confidence, warnings, floor) {
|
|
901
|
+
const fc = checkFields(answer, ranked);
|
|
902
|
+
if (fc.checked === 0 || fc.rate === 1) {
|
|
903
|
+
return { confidence, warnings };
|
|
904
|
+
}
|
|
905
|
+
const adjusted = Math.max(0, confidence * (0.5 + 0.5 * fc.rate));
|
|
906
|
+
const next = [...warnings];
|
|
907
|
+
if (fc.ungrounded.length > 0) {
|
|
908
|
+
next.push({
|
|
909
|
+
code: "UNGROUNDED_TERMS",
|
|
910
|
+
message: `Answer mentions ${fc.ungrounded.length} term(s) not found in sources: ${fc.ungrounded.slice(0, 5).join(", ")}`,
|
|
911
|
+
details: { ungrounded: fc.ungrounded, rate: Math.round(fc.rate * 1e3) / 1e3 }
|
|
912
|
+
});
|
|
418
913
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
914
|
+
if (adjusted < floor && !next.some((w) => w.code === "LOW_CONFIDENCE")) {
|
|
915
|
+
next.push({
|
|
916
|
+
code: "LOW_CONFIDENCE",
|
|
917
|
+
message: `Confidence ${adjusted.toFixed(2)} is below the floor ${floor.toFixed(2)}; the answer may be unreliable.`
|
|
918
|
+
});
|
|
424
919
|
}
|
|
425
|
-
return
|
|
920
|
+
return { confidence: adjusted, warnings: next };
|
|
426
921
|
}
|
|
427
|
-
|
|
922
|
+
|
|
923
|
+
// src/answer/decompose.ts
|
|
924
|
+
async function decompose(query, llm, maxSubqueries) {
|
|
925
|
+
if (maxSubqueries <= 0) {
|
|
926
|
+
return [query];
|
|
927
|
+
}
|
|
928
|
+
const prompt = `Break the following question into at most ${maxSubqueries} focused, self-contained sub-questions that together cover it. Return one sub-question per line, no numbering.
|
|
929
|
+
|
|
930
|
+
Question: ${query}`;
|
|
428
931
|
try {
|
|
429
|
-
const content = await
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
932
|
+
const { content } = await llm.complete(prompt, { temperature: 0.2, maxTokens: 256 });
|
|
933
|
+
const subs = (content ?? "").split("\n").map((l) => l.replace(/^[-*\d.)\s]+/, "").trim()).filter((l) => l.length > 0).slice(0, maxSubqueries);
|
|
934
|
+
return subs.length > 0 ? [query, ...subs] : [query];
|
|
935
|
+
} catch {
|
|
936
|
+
return [query];
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/answer/synthesize.ts
|
|
941
|
+
var MAX_SNIPPET_CHARS = 600;
|
|
942
|
+
function snippetFrom(text, mode) {
|
|
943
|
+
if (mode === "none") {
|
|
944
|
+
return void 0;
|
|
945
|
+
}
|
|
946
|
+
if (mode === "full") {
|
|
947
|
+
return text.length > MAX_SNIPPET_CHARS ? `${text.slice(0, MAX_SNIPPET_CHARS)}\u2026` : text;
|
|
948
|
+
}
|
|
949
|
+
const line = text.split("\n").find((l) => l.trim().length > 8)?.trim() ?? text.trim().slice(0, 120);
|
|
950
|
+
return line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
951
|
+
}
|
|
952
|
+
function toSearchResults(ranked, opts) {
|
|
953
|
+
return ranked.map(({ chunk, matchedBy }) => ({
|
|
954
|
+
file: chunk.path,
|
|
955
|
+
lines: [chunk.startLine, chunk.endLine],
|
|
956
|
+
kind: chunk.kind,
|
|
957
|
+
matchedBy,
|
|
958
|
+
stale: opts.staleByFile.get(chunk.path) ?? false,
|
|
959
|
+
snippet: snippetFrom(chunk.text, opts.snippet)
|
|
960
|
+
}));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/answer/answer.ts
|
|
964
|
+
var MAX_SNIPPET_CHARS2 = 600;
|
|
965
|
+
function truncate(text) {
|
|
966
|
+
return text.length > MAX_SNIPPET_CHARS2 ? `${text.slice(0, MAX_SNIPPET_CHARS2)}\u2026` : text;
|
|
967
|
+
}
|
|
968
|
+
function extractiveAnswer(query, ranked) {
|
|
969
|
+
if (ranked.length === 0) {
|
|
970
|
+
return `No indexed content matched the query: "${query}".`;
|
|
971
|
+
}
|
|
972
|
+
const top = ranked[0].chunk;
|
|
973
|
+
return `Most relevant source: ${top.path} (lines ${top.startLine}-${top.endLine}).
|
|
974
|
+
|
|
975
|
+
${truncate(top.text)}`;
|
|
976
|
+
}
|
|
977
|
+
async function synthesizeAnswer(query, ranked, llm, useLLM) {
|
|
978
|
+
if (!useLLM || ranked.length === 0) {
|
|
979
|
+
return extractiveAnswer(query, ranked);
|
|
980
|
+
}
|
|
981
|
+
const context = ranked.map((r, i) => `[${i + 1}] ${r.chunk.path}:${r.chunk.startLine}-${r.chunk.endLine}
|
|
982
|
+
${r.chunk.text}`).join("\n\n");
|
|
983
|
+
const prompt = `Answer the question using ONLY the provided context. Be concise and cite the relevant files by path. If the context does not contain the answer, say so.
|
|
984
|
+
|
|
985
|
+
Question: ${query}
|
|
986
|
+
|
|
987
|
+
Context:
|
|
988
|
+
${context}`;
|
|
989
|
+
try {
|
|
990
|
+
const { content } = await llm.complete(prompt, { temperature: 0.2, maxTokens: 800 });
|
|
991
|
+
const trimmed = (content ?? "").trim();
|
|
992
|
+
return trimmed.length > 0 ? trimmed : extractiveAnswer(query, ranked);
|
|
993
|
+
} catch {
|
|
994
|
+
return extractiveAnswer(query, ranked);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function toSources(ranked, opts) {
|
|
998
|
+
return ranked.map(({ chunk, matchedBy }) => ({
|
|
999
|
+
file: chunk.path,
|
|
1000
|
+
lines: [chunk.startLine, chunk.endLine],
|
|
1001
|
+
kind: chunk.kind,
|
|
1002
|
+
matchedBy,
|
|
1003
|
+
stale: opts.staleByFile.get(chunk.path) ?? false,
|
|
1004
|
+
snippet: snippetFrom(chunk.text, opts.snippet)
|
|
1005
|
+
}));
|
|
1006
|
+
}
|
|
1007
|
+
function buildAgentResponse(input) {
|
|
1008
|
+
const sources = toSources(input.ranked, { snippet: input.snippet, staleByFile: input.staleByFile });
|
|
1009
|
+
const abstained = input.confidence < input.floor || sources.length === 0;
|
|
1010
|
+
const response = {
|
|
1011
|
+
answer: input.answer,
|
|
1012
|
+
confidence: input.confidence,
|
|
1013
|
+
abstained,
|
|
1014
|
+
sources,
|
|
1015
|
+
warnings: input.warnings && input.warnings.length > 0 ? input.warnings : void 0,
|
|
1016
|
+
meta: {
|
|
1017
|
+
requestId: input.requestId,
|
|
1018
|
+
mode: input.mode,
|
|
1019
|
+
timingMs: input.timingMs,
|
|
1020
|
+
indexId: input.indexId
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
return AgentResponseSchema.parse(response);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/answer/explore.ts
|
|
1027
|
+
function toExploreEntries(ranked, staleByFile) {
|
|
1028
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1029
|
+
const out = [];
|
|
1030
|
+
for (const r of ranked) {
|
|
1031
|
+
if (seen.has(r.chunk.path)) {
|
|
1032
|
+
continue;
|
|
451
1033
|
}
|
|
1034
|
+
seen.add(r.chunk.path);
|
|
1035
|
+
const line = snippetFrom(r.chunk.text, "line");
|
|
1036
|
+
out.push({
|
|
1037
|
+
file: r.chunk.path,
|
|
1038
|
+
lines: [r.chunk.startLine, r.chunk.endLine],
|
|
1039
|
+
why: line && line.length > 0 ? line : `${r.chunk.kind} file`,
|
|
1040
|
+
matchedBy: r.matchedBy,
|
|
1041
|
+
stale: staleByFile.get(r.chunk.path) ?? false
|
|
1042
|
+
});
|
|
452
1043
|
}
|
|
453
|
-
|
|
1044
|
+
return out;
|
|
454
1045
|
}
|
|
455
|
-
function
|
|
456
|
-
|
|
457
|
-
|
|
1046
|
+
function spreadOf(files) {
|
|
1047
|
+
return new Set(
|
|
1048
|
+
files.map((f) => {
|
|
1049
|
+
const i = f.lastIndexOf("/");
|
|
1050
|
+
return i < 0 ? "." : f.slice(0, i);
|
|
1051
|
+
})
|
|
1052
|
+
).size;
|
|
458
1053
|
}
|
|
459
|
-
async function
|
|
460
|
-
|
|
1054
|
+
async function orientationSummary(task, entries, llm, useLLM) {
|
|
1055
|
+
if (!useLLM || entries.length === 0) {
|
|
1056
|
+
return "";
|
|
1057
|
+
}
|
|
1058
|
+
const list = entries.map((e, i) => `[${i + 1}] ${e.file}:${e.lines[0]}-${e.lines[1]} \u2014 ${e.why}`).join("\n");
|
|
1059
|
+
const prompt = `A developer must approach the task below in an unfamiliar codebase. Using ONLY the relevant files listed, briefly explain WHERE to start, the KEY files and their role, and HOW involved the task looks. Be concise; cite files by path.
|
|
1060
|
+
|
|
1061
|
+
Task: ${task}
|
|
1062
|
+
|
|
1063
|
+
Relevant files:
|
|
1064
|
+
${list}`;
|
|
461
1065
|
try {
|
|
462
|
-
const
|
|
463
|
-
|
|
1066
|
+
const { content } = await llm.complete(prompt, { temperature: 0.2, maxTokens: 400 });
|
|
1067
|
+
return (content ?? "").trim();
|
|
1068
|
+
} catch {
|
|
1069
|
+
return "";
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function historyKey(indexId) {
|
|
1073
|
+
return `${MIND_NAMESPACE_PREFIX}history:${indexId}`;
|
|
1074
|
+
}
|
|
1075
|
+
async function recordQuery(cache, indexId, query, at) {
|
|
1076
|
+
try {
|
|
1077
|
+
await cache.zadd(historyKey(indexId), at, query);
|
|
1078
|
+
} catch {
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async function recentQueries(cache, indexId, sinceMs = 0) {
|
|
1082
|
+
try {
|
|
1083
|
+
return await cache.zrangebyscore(historyKey(indexId), sinceMs, Number.MAX_SAFE_INTEGER);
|
|
1084
|
+
} catch {
|
|
1085
|
+
return [];
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/mind.ts
|
|
1090
|
+
function createMind(services, config, options = {}) {
|
|
1091
|
+
const now = options.now ?? (() => Date.now());
|
|
1092
|
+
const isoNow = options.isoNow ?? (() => new Date(now()).toISOString());
|
|
1093
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1094
|
+
function resolveIndexId(indexId) {
|
|
1095
|
+
return indexId && indexId.trim() !== "" ? indexId : config.defaultIndex;
|
|
1096
|
+
}
|
|
1097
|
+
function syncOpts(indexId) {
|
|
1098
|
+
const eff = effectiveIndexConfig(config, indexId);
|
|
1099
|
+
return {
|
|
1100
|
+
indexId,
|
|
1101
|
+
cwd,
|
|
1102
|
+
chunk: { maxTokens: eff.chunk.maxTokens, overlapTokens: eff.chunk.overlapTokens },
|
|
1103
|
+
ast: eff.chunk.ast,
|
|
1104
|
+
now: isoNow()
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
async index(req, onProgress) {
|
|
1109
|
+
const indexId = resolveIndexId(req.indexId);
|
|
1110
|
+
const eff = effectiveIndexConfig(config, indexId);
|
|
1111
|
+
const start = now();
|
|
1112
|
+
const result = await ingest(
|
|
1113
|
+
{
|
|
1114
|
+
indexId,
|
|
1115
|
+
cwd,
|
|
1116
|
+
// CLI `--scope` overrides the include set (keeping configured excludes);
|
|
1117
|
+
// otherwise use the index's configured include/exclude scope.
|
|
1118
|
+
scope: req.scope ? { include: [req.scope], exclude: eff.scope.exclude } : eff.scope,
|
|
1119
|
+
full: req.full,
|
|
1120
|
+
chunk: { maxTokens: eff.chunk.maxTokens, overlapTokens: eff.chunk.overlapTokens },
|
|
1121
|
+
ast: eff.chunk.ast,
|
|
1122
|
+
now: isoNow(),
|
|
1123
|
+
onProgress
|
|
1124
|
+
},
|
|
1125
|
+
services
|
|
1126
|
+
);
|
|
1127
|
+
const durationMs = now() - start;
|
|
1128
|
+
services.logger?.info("mind: index", {
|
|
1129
|
+
indexId,
|
|
1130
|
+
filesIndexed: result.filesIndexed,
|
|
1131
|
+
chunks: result.chunks,
|
|
1132
|
+
added: result.added,
|
|
1133
|
+
updated: result.updated,
|
|
1134
|
+
removed: result.removed,
|
|
1135
|
+
unchanged: result.unchanged,
|
|
1136
|
+
durationMs
|
|
1137
|
+
});
|
|
1138
|
+
return { indexId, filesIndexed: result.filesIndexed, chunks: result.chunks, durationMs };
|
|
1139
|
+
},
|
|
1140
|
+
async search(req) {
|
|
1141
|
+
const indexId = resolveIndexId(req.indexId);
|
|
1142
|
+
const retrieval = effectiveIndexConfig(config, indexId).retrieval;
|
|
1143
|
+
const limit = req.limit ?? retrieval.limit;
|
|
1144
|
+
const snippet = req.snippet ?? "line";
|
|
1145
|
+
const t0 = now();
|
|
1146
|
+
const retrieved = await retrieve(
|
|
1147
|
+
{ text: req.text, indexId, limit: limit * 3, intent: req.intent, rrfK: retrieval.rrfK, hyde: retrieval.hyde, expand: retrieval.expand },
|
|
1148
|
+
services
|
|
1149
|
+
);
|
|
1150
|
+
let ranked = retrieval.rerank ? rerank(retrieved.ranked, req.text) : retrieved.ranked;
|
|
1151
|
+
if (retrieval.dedup) {
|
|
1152
|
+
ranked = dedupRanked(ranked);
|
|
1153
|
+
}
|
|
1154
|
+
ranked = ranked.slice(0, limit);
|
|
1155
|
+
const manifest = await loadManifest(services.storage, indexId);
|
|
1156
|
+
const stales = await staleMap(manifest, ranked.map((r) => r.chunk.path), cwd);
|
|
1157
|
+
const results = toSearchResults(ranked, { snippet, staleByFile: stales });
|
|
1158
|
+
const staleCount2 = [...stales.values()].filter(Boolean).length;
|
|
1159
|
+
const timingMs = now() - t0;
|
|
1160
|
+
services.logger?.info("mind: search", {
|
|
1161
|
+
indexId,
|
|
1162
|
+
results: results.length,
|
|
1163
|
+
semanticWinRate: retrieved.semanticWinRate,
|
|
1164
|
+
staleCount: staleCount2,
|
|
1165
|
+
timingMs
|
|
1166
|
+
});
|
|
464
1167
|
return {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1168
|
+
results,
|
|
1169
|
+
confidence: retrieved.confidence,
|
|
1170
|
+
indexId,
|
|
1171
|
+
meta: { requestId: `mind:${t0}`, timingMs, semanticWinRate: retrieved.semanticWinRate, staleCount: staleCount2 }
|
|
469
1172
|
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
} else if (index.apiIndexHash) {
|
|
486
|
-
inconsistencies.push("API index file missing but hash is present");
|
|
487
|
-
}
|
|
488
|
-
if (depsGraph) {
|
|
489
|
-
const computedHash = computeJsonHash(depsGraph);
|
|
490
|
-
if (computedHash !== index.depsHash) {
|
|
491
|
-
inconsistencies.push(
|
|
492
|
-
`Dependencies hash mismatch: expected ${index.depsHash}, got ${computedHash}`
|
|
1173
|
+
},
|
|
1174
|
+
async ask(req) {
|
|
1175
|
+
const indexId = resolveIndexId(req.indexId);
|
|
1176
|
+
const retrieval = effectiveIndexConfig(config, indexId).retrieval;
|
|
1177
|
+
const mode = req.mode ?? "auto";
|
|
1178
|
+
const budget = config.modes[mode];
|
|
1179
|
+
const t0 = now();
|
|
1180
|
+
await recordQuery(services.cache, indexId, req.text, t0);
|
|
1181
|
+
const queries = budget.useLLM ? await decompose(req.text, services.llm, budget.maxSubqueries) : [req.text];
|
|
1182
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1183
|
+
let retrievalConfidence = 0;
|
|
1184
|
+
for (const q of queries) {
|
|
1185
|
+
const r = await retrieve(
|
|
1186
|
+
{ text: q, indexId, limit: budget.maxChunks, intent: void 0, rrfK: retrieval.rrfK, hyde: retrieval.hyde, expand: retrieval.expand },
|
|
1187
|
+
services
|
|
493
1188
|
);
|
|
1189
|
+
retrievalConfidence = Math.max(retrievalConfidence, r.confidence);
|
|
1190
|
+
for (const rc of r.ranked) {
|
|
1191
|
+
const prev = merged.get(rc.chunk.id);
|
|
1192
|
+
if (!prev || rc.score > prev.score) {
|
|
1193
|
+
merged.set(rc.chunk.id, rc);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
494
1196
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (recentDiff) {
|
|
499
|
-
const computedHash = computeJsonHash(recentDiff);
|
|
500
|
-
if (computedHash !== index.recentDiffHash) {
|
|
501
|
-
inconsistencies.push(
|
|
502
|
-
`Recent diff hash mismatch: expected ${index.recentDiffHash}, got ${computedHash}`
|
|
503
|
-
);
|
|
1197
|
+
let ranked = retrieval.rerank ? rerank([...merged.values()], req.text) : [...merged.values()].sort((a, b) => b.score - a.score);
|
|
1198
|
+
if (retrieval.dedup) {
|
|
1199
|
+
ranked = dedupRanked(ranked);
|
|
504
1200
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1201
|
+
ranked = ranked.slice(0, budget.maxChunks);
|
|
1202
|
+
const verification = await verifySources(ranked, services.storage);
|
|
1203
|
+
const base = computeConfidence(retrievalConfidence, verification.rate, config.confidence.floor);
|
|
1204
|
+
const answer = await synthesizeAnswer(req.text, ranked, services.llm, budget.useLLM);
|
|
1205
|
+
const { confidence, warnings } = budget.useLLM ? applyFieldCheck(answer, ranked, base.confidence, base.warnings, config.confidence.floor) : base;
|
|
1206
|
+
const manifest = await loadManifest(services.storage, indexId);
|
|
1207
|
+
const staleByFile = await staleMap(manifest, ranked.map((r) => r.chunk.path), cwd);
|
|
1208
|
+
const timingMs = now() - t0;
|
|
1209
|
+
services.logger?.info("mind: ask", {
|
|
1210
|
+
indexId,
|
|
1211
|
+
mode,
|
|
1212
|
+
subqueries: queries.length,
|
|
1213
|
+
chunks: ranked.length,
|
|
1214
|
+
confidence: Math.round(confidence * 1e3) / 1e3,
|
|
1215
|
+
timingMs
|
|
1216
|
+
});
|
|
1217
|
+
return buildAgentResponse({
|
|
1218
|
+
answer,
|
|
1219
|
+
ranked,
|
|
1220
|
+
confidence,
|
|
1221
|
+
mode,
|
|
1222
|
+
requestId: `mind:${t0}`,
|
|
1223
|
+
timingMs,
|
|
1224
|
+
indexId,
|
|
1225
|
+
floor: config.confidence.floor,
|
|
1226
|
+
snippet: req.snippet ?? "line",
|
|
1227
|
+
staleByFile,
|
|
1228
|
+
warnings
|
|
1229
|
+
});
|
|
1230
|
+
},
|
|
1231
|
+
async explore(req) {
|
|
1232
|
+
const indexId = resolveIndexId(req.indexId);
|
|
1233
|
+
const retrieval = effectiveIndexConfig(config, indexId).retrieval;
|
|
1234
|
+
const budget = config.modes.auto;
|
|
1235
|
+
const limit = req.limit ?? retrieval.limit;
|
|
1236
|
+
const t0 = now();
|
|
1237
|
+
const retrieved = await retrieve(
|
|
1238
|
+
{ text: req.task, indexId, limit: limit * 3, intent: "architecture", rrfK: retrieval.rrfK, hyde: retrieval.hyde, expand: retrieval.expand },
|
|
1239
|
+
services
|
|
519
1240
|
);
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1241
|
+
let ranked = retrieval.rerank ? rerank(retrieved.ranked, req.task) : retrieved.ranked;
|
|
1242
|
+
if (retrieval.dedup) {
|
|
1243
|
+
ranked = dedupRanked(ranked);
|
|
1244
|
+
}
|
|
1245
|
+
ranked = ranked.slice(0, limit);
|
|
1246
|
+
const manifest = await loadManifest(services.storage, indexId);
|
|
1247
|
+
const stales = await staleMap(manifest, ranked.map((r) => r.chunk.path), cwd);
|
|
1248
|
+
const files = toExploreEntries(ranked, stales);
|
|
1249
|
+
const spread = spreadOf(files.map((f) => f.file));
|
|
1250
|
+
const summary = await orientationSummary(req.task, files, services.llm, budget.useLLM);
|
|
1251
|
+
const timingMs = now() - t0;
|
|
1252
|
+
services.logger?.info("mind: explore", {
|
|
1253
|
+
indexId,
|
|
1254
|
+
filesTouched: files.length,
|
|
1255
|
+
spread,
|
|
1256
|
+
timingMs
|
|
1257
|
+
});
|
|
1258
|
+
return {
|
|
1259
|
+
task: req.task,
|
|
1260
|
+
indexId,
|
|
1261
|
+
confidence: retrieved.confidence,
|
|
1262
|
+
summary,
|
|
1263
|
+
files,
|
|
1264
|
+
meta: { requestId: `mind:${t0}`, timingMs, filesTouched: files.length, spread }
|
|
1265
|
+
};
|
|
1266
|
+
},
|
|
1267
|
+
async reindex(req) {
|
|
1268
|
+
return this.index({ indexId: req.indexId, full: true });
|
|
1269
|
+
},
|
|
1270
|
+
async drop(req) {
|
|
1271
|
+
const indexId = resolveIndexId(req.indexId);
|
|
1272
|
+
if (!await manifestExists(services.storage, indexId)) {
|
|
1273
|
+
throw new Error(`No such index "${indexId}" \u2014 nothing to drop (run \`kb mind status\` to list indexes).`);
|
|
1274
|
+
}
|
|
1275
|
+
const manifest = await loadManifest(services.storage, indexId);
|
|
1276
|
+
const ids = manifest.chunks.map((c) => c.id);
|
|
1277
|
+
const droppedFiles = Object.keys(manifest.files).length;
|
|
1278
|
+
const BATCH = 500;
|
|
1279
|
+
for (let i = 0; i < ids.length; i += BATCH) {
|
|
1280
|
+
await services.vectorStore.delete(ids.slice(i, i + BATCH), indexId);
|
|
1281
|
+
}
|
|
1282
|
+
await deleteManifest(services.storage, indexId);
|
|
1283
|
+
services.logger?.info("mind: drop", { indexId, droppedChunks: ids.length, droppedFiles });
|
|
1284
|
+
return { indexId, droppedChunks: ids.length, droppedFiles };
|
|
1285
|
+
},
|
|
1286
|
+
async syncAdd(paths, indexId) {
|
|
1287
|
+
const id = resolveIndexId(indexId);
|
|
1288
|
+
const counts = await syncAdd(paths, services, syncOpts(id));
|
|
1289
|
+
return { indexId: id, ...counts };
|
|
1290
|
+
},
|
|
1291
|
+
async syncUpdate(paths, indexId) {
|
|
1292
|
+
const id = resolveIndexId(indexId);
|
|
1293
|
+
const counts = await syncUpdate(paths, services, syncOpts(id));
|
|
1294
|
+
return { indexId: id, ...counts };
|
|
1295
|
+
},
|
|
1296
|
+
async syncDelete(paths, indexId) {
|
|
1297
|
+
const id = resolveIndexId(indexId);
|
|
1298
|
+
const counts = await syncDelete(paths, services, syncOpts(id));
|
|
1299
|
+
return { indexId: id, ...counts };
|
|
1300
|
+
},
|
|
1301
|
+
async syncList(indexId) {
|
|
1302
|
+
const id = resolveIndexId(indexId);
|
|
1303
|
+
const manifest = await loadManifest(services.storage, id);
|
|
1304
|
+
return {
|
|
1305
|
+
indexId: id,
|
|
1306
|
+
documents: Object.entries(manifest.files).map(([path2, info]) => ({
|
|
1307
|
+
path: path2,
|
|
1308
|
+
chunks: info.chunks,
|
|
1309
|
+
indexedAt: info.indexedAt
|
|
1310
|
+
}))
|
|
1311
|
+
};
|
|
1312
|
+
},
|
|
1313
|
+
async syncStatus(indexId) {
|
|
1314
|
+
const id = resolveIndexId(indexId);
|
|
1315
|
+
const manifest = await loadManifest(services.storage, id);
|
|
1316
|
+
return {
|
|
1317
|
+
indexId: id,
|
|
1318
|
+
documents: Object.keys(manifest.files).length,
|
|
1319
|
+
chunks: manifest.chunks.length,
|
|
1320
|
+
lastIndexedAt: manifest.updatedAt,
|
|
1321
|
+
stale: false
|
|
1322
|
+
};
|
|
1323
|
+
},
|
|
1324
|
+
async status(indexId) {
|
|
1325
|
+
const ids = indexId ? [indexId] : [.../* @__PURE__ */ new Set([...Object.keys(config.indexes), ...await listIndexIds(services)])];
|
|
1326
|
+
const indexes = [];
|
|
1327
|
+
for (const id of ids) {
|
|
1328
|
+
const manifest = await loadManifest(services.storage, id);
|
|
1329
|
+
const declared = config.indexes[id];
|
|
1330
|
+
const sc = resolveScope(declared?.scope);
|
|
1331
|
+
const coverage = sc.include.join(", ") + (sc.exclude.length ? ` \xB7 excl ${sc.exclude.join(", ")}` : "");
|
|
1332
|
+
indexes.push({
|
|
1333
|
+
indexId: id,
|
|
1334
|
+
label: declared?.label,
|
|
1335
|
+
coverage,
|
|
1336
|
+
documents: Object.keys(manifest.files).length,
|
|
1337
|
+
chunks: manifest.chunks.length,
|
|
1338
|
+
lastIndexedAt: manifest.updatedAt,
|
|
1339
|
+
staleCount: await staleCount(manifest, cwd)
|
|
1340
|
+
});
|
|
527
1341
|
}
|
|
1342
|
+
return { indexes, healthy: true };
|
|
1343
|
+
},
|
|
1344
|
+
async health() {
|
|
1345
|
+
return {
|
|
1346
|
+
ok: true,
|
|
1347
|
+
vectorStore: Boolean(services.vectorStore),
|
|
1348
|
+
embeddings: Boolean(services.embeddings),
|
|
1349
|
+
llm: Boolean(services.llm)
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
async function listIndexIds(services) {
|
|
1355
|
+
const paths = await services.storage.list("mind/");
|
|
1356
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1357
|
+
for (const p of paths) {
|
|
1358
|
+
const match = /^mind\/([^/]+)\/manifest\.json$/.exec(p);
|
|
1359
|
+
if (match) {
|
|
1360
|
+
ids.add(match[1]);
|
|
528
1361
|
}
|
|
529
|
-
const ok = inconsistencies.length === 0;
|
|
530
|
-
const code = ok ? null : "MIND_INDEX_INCONSISTENT";
|
|
531
|
-
const hint = ok ? "All indexes are consistent and up to date" : 'Run "kb mind rag-index" to rebuild indexes';
|
|
532
|
-
return { ok, code, inconsistencies, hint };
|
|
533
|
-
} catch (error) {
|
|
534
|
-
return {
|
|
535
|
-
ok: false,
|
|
536
|
-
code: "MIND_VERIFY_ERROR",
|
|
537
|
-
inconsistencies: [`Verification failed: ${error instanceof Error ? error.message : String(error)}`],
|
|
538
|
-
hint: "Check file permissions and workspace structure"
|
|
539
|
-
};
|
|
540
1362
|
}
|
|
1363
|
+
return [...ids];
|
|
541
1364
|
}
|
|
542
1365
|
|
|
543
|
-
// src/
|
|
544
|
-
var
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1366
|
+
// src/pipeline.ts
|
|
1367
|
+
var defaultClock = () => Date.now();
|
|
1368
|
+
var Tracer = class {
|
|
1369
|
+
constructor(requestId, mode, clock = defaultClock) {
|
|
1370
|
+
this.requestId = requestId;
|
|
1371
|
+
this.mode = mode;
|
|
1372
|
+
this.clock = clock;
|
|
1373
|
+
}
|
|
1374
|
+
requestId;
|
|
1375
|
+
mode;
|
|
1376
|
+
clock;
|
|
1377
|
+
stages = [];
|
|
1378
|
+
async run(stage, input, services, fn) {
|
|
1379
|
+
const start = this.clock();
|
|
1380
|
+
const output = await fn(input, services);
|
|
1381
|
+
this.stages.push({
|
|
1382
|
+
stage,
|
|
1383
|
+
durationMs: this.clock() - start,
|
|
1384
|
+
outputCount: Array.isArray(output) ? output.length : void 0
|
|
1385
|
+
});
|
|
1386
|
+
return output;
|
|
1387
|
+
}
|
|
1388
|
+
/** Record a stage trace manually (for stages not wrapped by `run`). */
|
|
1389
|
+
record(trace) {
|
|
1390
|
+
this.stages.push(trace);
|
|
1391
|
+
}
|
|
1392
|
+
build(totalMs) {
|
|
1393
|
+
return {
|
|
1394
|
+
requestId: this.requestId,
|
|
1395
|
+
mode: this.mode,
|
|
1396
|
+
totalMs,
|
|
1397
|
+
stages: [...this.stages]
|
|
1398
|
+
};
|
|
568
1399
|
}
|
|
569
1400
|
};
|
|
570
|
-
var DEFAULT_TIME_BUDGET_MS = 800;
|
|
571
|
-
var MAX_FILE_SIZE_BYTES = 1.5 * 1024 * 1024;
|
|
572
|
-
var MAX_SNIPPET_LINES = 60;
|
|
573
|
-
function getGenerator() {
|
|
574
|
-
return "kb-labs-mind@0.1.0";
|
|
575
|
-
}
|
|
576
1401
|
|
|
577
|
-
export {
|
|
1402
|
+
export { Tracer, bm25Search, buildAgentResponse, checkFields, chunkFile, chunkId, computeConfidence, createMind, decompose, dedupRanked, extractSymbols, ingest, intentWeights, kindFromPath, loadManifest, recentQueries, recordQuery, rerank, retrieve, rrfFuse, saveManifest, slidingWindowChunks, structuralChunks, syncAdd, syncDelete, syncUpdate, synthesizeAnswer, toSearchResults, toSources, tokenize, verifySources };
|
|
578
1403
|
//# sourceMappingURL=index.js.map
|
|
579
1404
|
//# sourceMappingURL=index.js.map
|