wendkeep 0.85.0 → 0.86.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/CHANGELOG.md +40 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/en/commands/evidence-embeddings.md +243 -0
- package/docs/en/commands/mcp.md +67 -7
- package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
- package/docs/pt-BR/commands/mcp.md +66 -7
- package/hooks/evidence-context.mjs +41 -7
- package/hooks/evidence-recall.mjs +10 -0
- package/package.json +1 -1
- package/packages/mcp/src/effects.mjs +3 -2
- package/packages/mcp/src/evidence-recall.mjs +130 -0
- package/packages/mcp/src/executor.mjs +4 -0
- package/packages/mcp/src/server.mjs +31 -1
- package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
- package/packages/vault/src/evidence-index-store.mjs +360 -0
- package/packages/vault/src/evidence-recall-page.mjs +381 -0
- package/packages/vault/src/evidence-search-index.mjs +917 -0
- package/packages/vault/src/index.mjs +12 -1
- package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
- package/packages/vault/src/memory-ledger-view.mjs +41 -0
- package/packages/vault/src/memory-rotation-store.mjs +967 -0
- package/packages/vault/src/memory-segment-store.mjs +820 -0
- package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
- package/packages/vault/src/memory-store-base.mjs +1161 -0
- package/packages/vault/src/memory-store-core.mjs +2 -0
- package/packages/vault/src/memory-store.mjs +46 -1161
- package/src/doctor.mjs +41 -5
- package/src/evidence-search-health.mjs +221 -0
- package/src/memory-scale-health.mjs +210 -0
- package/src/observer-snapshot.mjs +87 -1
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const EVIDENCE_EMBEDDING_PROTOCOL_VERSION = 1;
|
|
4
|
+
export const EVIDENCE_EMBEDDING_DEFAULT_MAX_CANDIDATES = 128;
|
|
5
|
+
export const EVIDENCE_EMBEDDING_MAX_CANDIDATES = 512;
|
|
6
|
+
export const EVIDENCE_EMBEDDING_DEFAULT_MAX_INPUT_BYTES = 256 * 1024;
|
|
7
|
+
export const EVIDENCE_EMBEDDING_MAX_INPUT_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
export const EVIDENCE_EMBEDDING_MAX_DIMENSIONS = 65_536;
|
|
9
|
+
|
|
10
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
11
|
+
const IDENTIFIER = /^[a-z][a-z0-9._-]{1,127}$/;
|
|
12
|
+
const MANIFEST_KEYS = [
|
|
13
|
+
'dimensions',
|
|
14
|
+
'integrity',
|
|
15
|
+
'locality',
|
|
16
|
+
'max_batch_size',
|
|
17
|
+
'max_input_bytes',
|
|
18
|
+
'model_fingerprint',
|
|
19
|
+
'model_id',
|
|
20
|
+
'model_revision',
|
|
21
|
+
'network',
|
|
22
|
+
'plugin_id',
|
|
23
|
+
'plugin_version',
|
|
24
|
+
'protocol_version',
|
|
25
|
+
'retention',
|
|
26
|
+
'schema_version',
|
|
27
|
+
'transport',
|
|
28
|
+
];
|
|
29
|
+
const RESPONSE_KEYS = [
|
|
30
|
+
'document_vectors',
|
|
31
|
+
'model_fingerprint',
|
|
32
|
+
'query_vector',
|
|
33
|
+
'schema_version',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export class EvidenceEmbeddingPluginError extends Error {
|
|
37
|
+
constructor(message, errors = []) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'EvidenceEmbeddingPluginError';
|
|
40
|
+
this.code = 'EVIDENCE_EMBEDDING_PLUGIN_INVALID';
|
|
41
|
+
this.errors = [...errors];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class EvidenceEmbeddingBudgetError extends Error {
|
|
46
|
+
constructor(message, { requiredBytes = null, maxBytes = null } = {}) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'EvidenceEmbeddingBudgetError';
|
|
49
|
+
this.code = 'EVIDENCE_EMBEDDING_BUDGET_EXCEEDED';
|
|
50
|
+
this.required_bytes = requiredBytes;
|
|
51
|
+
this.max_bytes = maxBytes;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class EvidenceEmbeddingResponseError extends Error {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = 'EvidenceEmbeddingResponseError';
|
|
59
|
+
this.code = 'EVIDENCE_EMBEDDING_RESPONSE_INVALID';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class EvidenceEmbeddingExecutionError extends Error {
|
|
64
|
+
constructor(message, cause = null) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = 'EvidenceEmbeddingExecutionError';
|
|
67
|
+
this.code = 'EVIDENCE_EMBEDDING_EXECUTION_FAILED';
|
|
68
|
+
this.cause = cause;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function compareText(left, right) {
|
|
73
|
+
const a = String(left ?? '');
|
|
74
|
+
const b = String(right ?? '');
|
|
75
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function canonicalize(value) {
|
|
79
|
+
if (Array.isArray(value)) return value.map((item) => canonicalize(item));
|
|
80
|
+
if (!value || typeof value !== 'object') return value;
|
|
81
|
+
return Object.fromEntries(Object.keys(value).sort(compareText)
|
|
82
|
+
.filter((key) => value[key] !== undefined)
|
|
83
|
+
.map((key) => [key, canonicalize(value[key])]));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stableJson(value) {
|
|
87
|
+
return JSON.stringify(canonicalize(value));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sha256(value) {
|
|
91
|
+
return `sha256:${createHash('sha256').update(String(value), 'utf8').digest('hex')}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function manifestPayload(manifest = {}) {
|
|
95
|
+
return {
|
|
96
|
+
schema_version: manifest.schema_version,
|
|
97
|
+
protocol_version: manifest.protocol_version,
|
|
98
|
+
plugin_id: manifest.plugin_id,
|
|
99
|
+
plugin_version: manifest.plugin_version,
|
|
100
|
+
model_id: manifest.model_id,
|
|
101
|
+
model_revision: manifest.model_revision,
|
|
102
|
+
model_fingerprint: manifest.model_fingerprint,
|
|
103
|
+
dimensions: manifest.dimensions,
|
|
104
|
+
locality: manifest.locality,
|
|
105
|
+
transport: manifest.transport,
|
|
106
|
+
network: manifest.network,
|
|
107
|
+
retention: manifest.retention,
|
|
108
|
+
max_batch_size: manifest.max_batch_size,
|
|
109
|
+
max_input_bytes: manifest.max_input_bytes,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function evidenceEmbeddingManifestIntegrity(manifest) {
|
|
114
|
+
return sha256(stableJson(manifestPayload(manifest)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function deepFreeze(value) {
|
|
118
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
119
|
+
Object.freeze(value);
|
|
120
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function positiveInteger(value, field, maximum) {
|
|
125
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) return field;
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function versionText(value) {
|
|
130
|
+
return typeof value === 'string'
|
|
131
|
+
&& value.length >= 1
|
|
132
|
+
&& value.length <= 64
|
|
133
|
+
&& /^[A-Za-z0-9][A-Za-z0-9.+_-]*$/.test(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function verifyEvidenceEmbeddingPlugin(plugin) {
|
|
137
|
+
const errors = [];
|
|
138
|
+
const manifest = plugin?.manifest;
|
|
139
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
140
|
+
errors.push('manifest');
|
|
141
|
+
return { valid: false, errors, expected_integrity: '' };
|
|
142
|
+
}
|
|
143
|
+
const keys = Object.keys(manifest).sort(compareText);
|
|
144
|
+
if (stableJson(keys) !== stableJson(MANIFEST_KEYS)) errors.push('manifest_fields');
|
|
145
|
+
if (manifest.schema_version !== 1) errors.push('schema_version');
|
|
146
|
+
if (manifest.protocol_version !== EVIDENCE_EMBEDDING_PROTOCOL_VERSION) errors.push('protocol_version');
|
|
147
|
+
if (!IDENTIFIER.test(String(manifest.plugin_id || ''))) errors.push('plugin_id');
|
|
148
|
+
if (!versionText(manifest.plugin_version)) errors.push('plugin_version');
|
|
149
|
+
if (!IDENTIFIER.test(String(manifest.model_id || ''))) errors.push('model_id');
|
|
150
|
+
if (!versionText(manifest.model_revision)) errors.push('model_revision');
|
|
151
|
+
if (!SHA256.test(String(manifest.model_fingerprint || ''))) errors.push('model_fingerprint');
|
|
152
|
+
const dimensionsError = positiveInteger(
|
|
153
|
+
manifest.dimensions,
|
|
154
|
+
'dimensions',
|
|
155
|
+
EVIDENCE_EMBEDDING_MAX_DIMENSIONS,
|
|
156
|
+
);
|
|
157
|
+
if (dimensionsError) errors.push(dimensionsError);
|
|
158
|
+
if (manifest.locality !== 'local') errors.push('locality');
|
|
159
|
+
if (manifest.transport !== 'in-process') errors.push('transport');
|
|
160
|
+
if (manifest.network !== 'forbidden') errors.push('network');
|
|
161
|
+
if (manifest.retention !== 'none') errors.push('retention');
|
|
162
|
+
const batchError = positiveInteger(
|
|
163
|
+
manifest.max_batch_size,
|
|
164
|
+
'max_batch_size',
|
|
165
|
+
EVIDENCE_EMBEDDING_MAX_CANDIDATES,
|
|
166
|
+
);
|
|
167
|
+
if (batchError) errors.push(batchError);
|
|
168
|
+
const bytesError = positiveInteger(
|
|
169
|
+
manifest.max_input_bytes,
|
|
170
|
+
'max_input_bytes',
|
|
171
|
+
EVIDENCE_EMBEDDING_MAX_INPUT_BYTES,
|
|
172
|
+
);
|
|
173
|
+
if (bytesError) errors.push(bytesError);
|
|
174
|
+
const expectedIntegrity = evidenceEmbeddingManifestIntegrity(manifest);
|
|
175
|
+
if (!SHA256.test(String(manifest.integrity || ''))
|
|
176
|
+
|| manifest.integrity !== expectedIntegrity) errors.push('integrity');
|
|
177
|
+
if (typeof plugin?.embed !== 'function') errors.push('embed');
|
|
178
|
+
return {
|
|
179
|
+
valid: errors.length === 0,
|
|
180
|
+
errors: [...new Set(errors)],
|
|
181
|
+
expected_integrity: expectedIntegrity,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function buildEvidenceEmbeddingManifest({
|
|
186
|
+
plugin_id: pluginId,
|
|
187
|
+
plugin_version: pluginVersion,
|
|
188
|
+
model_id: modelId,
|
|
189
|
+
model_revision: modelRevision,
|
|
190
|
+
model_fingerprint: modelFingerprint,
|
|
191
|
+
dimensions,
|
|
192
|
+
locality = 'local',
|
|
193
|
+
transport = 'in-process',
|
|
194
|
+
network = 'forbidden',
|
|
195
|
+
retention = 'none',
|
|
196
|
+
max_batch_size: maxBatchSize = EVIDENCE_EMBEDDING_DEFAULT_MAX_CANDIDATES,
|
|
197
|
+
max_input_bytes: maxInputBytes = EVIDENCE_EMBEDDING_DEFAULT_MAX_INPUT_BYTES,
|
|
198
|
+
} = {}) {
|
|
199
|
+
const payload = {
|
|
200
|
+
schema_version: 1,
|
|
201
|
+
protocol_version: EVIDENCE_EMBEDDING_PROTOCOL_VERSION,
|
|
202
|
+
plugin_id: pluginId,
|
|
203
|
+
plugin_version: pluginVersion,
|
|
204
|
+
model_id: modelId,
|
|
205
|
+
model_revision: modelRevision,
|
|
206
|
+
model_fingerprint: modelFingerprint,
|
|
207
|
+
dimensions,
|
|
208
|
+
locality,
|
|
209
|
+
transport,
|
|
210
|
+
network,
|
|
211
|
+
retention,
|
|
212
|
+
max_batch_size: maxBatchSize,
|
|
213
|
+
max_input_bytes: maxInputBytes,
|
|
214
|
+
};
|
|
215
|
+
const manifest = deepFreeze({
|
|
216
|
+
...payload,
|
|
217
|
+
integrity: evidenceEmbeddingManifestIntegrity(payload),
|
|
218
|
+
});
|
|
219
|
+
const validation = verifyEvidenceEmbeddingPlugin({ manifest, embed() {} });
|
|
220
|
+
if (!validation.valid) {
|
|
221
|
+
throw new EvidenceEmbeddingPluginError(
|
|
222
|
+
`invalid evidence embedding manifest: ${validation.errors.join(', ')}`,
|
|
223
|
+
validation.errors,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return manifest;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function createEvidenceEmbeddingPlugin({ manifest, embed } = {}) {
|
|
230
|
+
const normalizedManifest = manifest?.integrity
|
|
231
|
+
? deepFreeze({ ...manifest })
|
|
232
|
+
: buildEvidenceEmbeddingManifest(manifest || {});
|
|
233
|
+
const plugin = Object.freeze({ manifest: normalizedManifest, embed });
|
|
234
|
+
const validation = verifyEvidenceEmbeddingPlugin(plugin);
|
|
235
|
+
if (!validation.valid) {
|
|
236
|
+
throw new EvidenceEmbeddingPluginError(
|
|
237
|
+
`invalid evidence embedding plugin: ${validation.errors.join(', ')}`,
|
|
238
|
+
validation.errors,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return plugin;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeInteger(value, fallback, { min = 1, max } = {}) {
|
|
245
|
+
const number = Number(value ?? fallback);
|
|
246
|
+
if (!Number.isSafeInteger(number) || number < min || number > max) {
|
|
247
|
+
throw new RangeError(`embedding limit must be an integer between ${min} and ${max}`);
|
|
248
|
+
}
|
|
249
|
+
return number;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function candidateText(row) {
|
|
253
|
+
return [row?.title, row?.heading, row?.content]
|
|
254
|
+
.map((value) => String(value || '').trim())
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.join('\n\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function requestPrefix(manifest, query) {
|
|
260
|
+
return `{"schema_version":1,"model_fingerprint":${JSON.stringify(manifest.model_fingerprint)},"query":${JSON.stringify({ text: query })},"documents":[`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function requestBytes(prefix, documentJson, footer = ']}') {
|
|
264
|
+
const documents = documentJson.join(',');
|
|
265
|
+
return Buffer.byteLength(`${prefix}${documents}${footer}`, 'utf8');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function selectDocuments(rows, query, manifest, { maxCandidates, maxInputBytes }) {
|
|
269
|
+
const effectiveCandidates = Math.min(maxCandidates, manifest.max_batch_size);
|
|
270
|
+
const effectiveBytes = Math.min(maxInputBytes, manifest.max_input_bytes);
|
|
271
|
+
const prefix = requestPrefix(manifest, query);
|
|
272
|
+
const baseBytes = requestBytes(prefix, []);
|
|
273
|
+
if (baseBytes > effectiveBytes) {
|
|
274
|
+
throw new EvidenceEmbeddingBudgetError(
|
|
275
|
+
'embedding query exceeds the configured input byte budget',
|
|
276
|
+
{ requiredBytes: baseBytes, maxBytes: effectiveBytes },
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const documents = [];
|
|
280
|
+
const documentJson = [];
|
|
281
|
+
for (let index = 0; index < rows.length && documents.length < effectiveCandidates; index += 1) {
|
|
282
|
+
const row = rows[index];
|
|
283
|
+
const id = String(row?.chunk_id || '').trim();
|
|
284
|
+
const text = candidateText(row);
|
|
285
|
+
if (!id || !text) continue;
|
|
286
|
+
const document = { id, text };
|
|
287
|
+
const rendered = JSON.stringify(document);
|
|
288
|
+
const nextBytes = requestBytes(prefix, [...documentJson, rendered]);
|
|
289
|
+
if (nextBytes > effectiveBytes) break;
|
|
290
|
+
documents.push({ id, text, row, original_index: index });
|
|
291
|
+
documentJson.push(rendered);
|
|
292
|
+
}
|
|
293
|
+
if (rows.length && !documents.length) {
|
|
294
|
+
const first = { id: String(rows[0]?.chunk_id || ''), text: candidateText(rows[0]) };
|
|
295
|
+
throw new EvidenceEmbeddingBudgetError(
|
|
296
|
+
'embedding byte budget cannot fit the first candidate',
|
|
297
|
+
{
|
|
298
|
+
requiredBytes: requestBytes(prefix, [JSON.stringify(first)]),
|
|
299
|
+
maxBytes: effectiveBytes,
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
documents,
|
|
305
|
+
inputBytes: requestBytes(prefix, documentJson),
|
|
306
|
+
maxCandidates: effectiveCandidates,
|
|
307
|
+
maxInputBytes: effectiveBytes,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function validVector(vector, dimensions) {
|
|
312
|
+
return Array.isArray(vector)
|
|
313
|
+
&& vector.length === dimensions
|
|
314
|
+
&& vector.every((value) => typeof value === 'number' && Number.isFinite(value));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function vectorNorm(vector) {
|
|
318
|
+
return Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function cosine(left, right) {
|
|
322
|
+
const leftNorm = vectorNorm(left);
|
|
323
|
+
const rightNorm = vectorNorm(right);
|
|
324
|
+
if (!leftNorm || !rightNorm) {
|
|
325
|
+
throw new EvidenceEmbeddingResponseError('embedding vectors must have a non-zero norm');
|
|
326
|
+
}
|
|
327
|
+
let dot = 0;
|
|
328
|
+
for (let index = 0; index < left.length; index += 1) dot += left[index] * right[index];
|
|
329
|
+
return dot / (leftNorm * rightNorm);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function validateResponse(response, manifest, documents) {
|
|
333
|
+
if (!response || typeof response !== 'object' || Array.isArray(response)) {
|
|
334
|
+
throw new EvidenceEmbeddingResponseError('embedding response must be an object');
|
|
335
|
+
}
|
|
336
|
+
if (stableJson(Object.keys(response).sort(compareText)) !== stableJson(RESPONSE_KEYS)) {
|
|
337
|
+
throw new EvidenceEmbeddingResponseError('embedding response contains unexpected fields');
|
|
338
|
+
}
|
|
339
|
+
if (response.schema_version !== 1) {
|
|
340
|
+
throw new EvidenceEmbeddingResponseError('embedding response schema version is unsupported');
|
|
341
|
+
}
|
|
342
|
+
if (response.model_fingerprint !== manifest.model_fingerprint) {
|
|
343
|
+
throw new EvidenceEmbeddingResponseError('embedding response model fingerprint diverges');
|
|
344
|
+
}
|
|
345
|
+
if (!validVector(response.query_vector, manifest.dimensions)) {
|
|
346
|
+
throw new EvidenceEmbeddingResponseError('embedding query vector is invalid');
|
|
347
|
+
}
|
|
348
|
+
if (!Array.isArray(response.document_vectors)
|
|
349
|
+
|| response.document_vectors.length !== documents.length) {
|
|
350
|
+
throw new EvidenceEmbeddingResponseError('embedding document vector count diverges');
|
|
351
|
+
}
|
|
352
|
+
const expectedIds = new Set(documents.map((document) => document.id));
|
|
353
|
+
const vectors = new Map();
|
|
354
|
+
for (const item of response.document_vectors) {
|
|
355
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)
|
|
356
|
+
|| stableJson(Object.keys(item).sort(compareText)) !== stableJson(['id', 'vector'])) {
|
|
357
|
+
throw new EvidenceEmbeddingResponseError('embedding document vector is invalid');
|
|
358
|
+
}
|
|
359
|
+
const id = String(item.id || '');
|
|
360
|
+
if (!expectedIds.has(id) || vectors.has(id)) {
|
|
361
|
+
throw new EvidenceEmbeddingResponseError('embedding document vector id diverges');
|
|
362
|
+
}
|
|
363
|
+
if (!validVector(item.vector, manifest.dimensions)) {
|
|
364
|
+
throw new EvidenceEmbeddingResponseError('embedding document vector dimensions are invalid');
|
|
365
|
+
}
|
|
366
|
+
vectors.set(id, item.vector);
|
|
367
|
+
}
|
|
368
|
+
return { queryVector: response.query_vector, vectors };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function baseMetrics(status, reason = '') {
|
|
372
|
+
return {
|
|
373
|
+
schema_version: 1,
|
|
374
|
+
status,
|
|
375
|
+
reason,
|
|
376
|
+
plugin_id: '',
|
|
377
|
+
plugin_version: '',
|
|
378
|
+
model_id: '',
|
|
379
|
+
model_revision: '',
|
|
380
|
+
model_fingerprint: '',
|
|
381
|
+
dimensions: 0,
|
|
382
|
+
requested_candidates: 0,
|
|
383
|
+
embedded_candidates: 0,
|
|
384
|
+
skipped_candidates: 0,
|
|
385
|
+
input_bytes: 0,
|
|
386
|
+
scores: [],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function pluginMetrics(manifest, rows, selection = null) {
|
|
391
|
+
return {
|
|
392
|
+
schema_version: 1,
|
|
393
|
+
status: 'applied',
|
|
394
|
+
reason: '',
|
|
395
|
+
plugin_id: manifest.plugin_id,
|
|
396
|
+
plugin_version: manifest.plugin_version,
|
|
397
|
+
model_id: manifest.model_id,
|
|
398
|
+
model_revision: manifest.model_revision,
|
|
399
|
+
model_fingerprint: manifest.model_fingerprint,
|
|
400
|
+
dimensions: manifest.dimensions,
|
|
401
|
+
requested_candidates: rows.length,
|
|
402
|
+
embedded_candidates: selection?.documents.length || 0,
|
|
403
|
+
skipped_candidates: Math.max(0, rows.length - (selection?.documents.length || 0)),
|
|
404
|
+
input_bytes: selection?.inputBytes || 0,
|
|
405
|
+
scores: [],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function fallback(rows, plugin, error) {
|
|
410
|
+
const manifest = plugin?.manifest;
|
|
411
|
+
return {
|
|
412
|
+
rows: [...rows],
|
|
413
|
+
metrics: {
|
|
414
|
+
...baseMetrics('fallback', String(error?.code || 'EVIDENCE_EMBEDDING_UNAVAILABLE')),
|
|
415
|
+
plugin_id: String(manifest?.plugin_id || ''),
|
|
416
|
+
plugin_version: String(manifest?.plugin_version || ''),
|
|
417
|
+
model_id: String(manifest?.model_id || ''),
|
|
418
|
+
model_revision: String(manifest?.model_revision || ''),
|
|
419
|
+
model_fingerprint: String(manifest?.model_fingerprint || ''),
|
|
420
|
+
dimensions: Number(manifest?.dimensions || 0),
|
|
421
|
+
requested_candidates: rows.length,
|
|
422
|
+
skipped_candidates: rows.length,
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function normalizePluginError(error) {
|
|
428
|
+
if (String(error?.code || '').startsWith('EVIDENCE_EMBEDDING_')) return error;
|
|
429
|
+
return new EvidenceEmbeddingExecutionError('evidence embedding plugin execution failed', error);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function rerankEvidenceCandidatesWithEmbedding(rows, query, {
|
|
433
|
+
enabled = false,
|
|
434
|
+
plugin = null,
|
|
435
|
+
required = false,
|
|
436
|
+
maxCandidates: requestedMaxCandidates,
|
|
437
|
+
maxInputBytes: requestedMaxInputBytes,
|
|
438
|
+
signal,
|
|
439
|
+
} = {}) {
|
|
440
|
+
if (!Array.isArray(rows)) throw new TypeError('embedding candidates must be an array');
|
|
441
|
+
const original = [...rows];
|
|
442
|
+
if (!enabled) {
|
|
443
|
+
return {
|
|
444
|
+
rows: original,
|
|
445
|
+
metrics: {
|
|
446
|
+
...baseMetrics('disabled', 'disabled'),
|
|
447
|
+
requested_candidates: original.length,
|
|
448
|
+
skipped_candidates: original.length,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (!String(query || '').trim()) {
|
|
453
|
+
const error = new TypeError('embedding query must not be empty');
|
|
454
|
+
if (required) throw error;
|
|
455
|
+
return fallback(original, plugin, error);
|
|
456
|
+
}
|
|
457
|
+
if (!plugin) {
|
|
458
|
+
const error = new EvidenceEmbeddingPluginError('evidence embedding plugin is required', ['plugin']);
|
|
459
|
+
if (required) throw error;
|
|
460
|
+
return fallback(original, plugin, error);
|
|
461
|
+
}
|
|
462
|
+
const validation = verifyEvidenceEmbeddingPlugin(plugin);
|
|
463
|
+
if (!validation.valid) {
|
|
464
|
+
const error = new EvidenceEmbeddingPluginError(
|
|
465
|
+
`invalid evidence embedding plugin: ${validation.errors.join(', ')}`,
|
|
466
|
+
validation.errors,
|
|
467
|
+
);
|
|
468
|
+
if (required) throw error;
|
|
469
|
+
return fallback(original, plugin, error);
|
|
470
|
+
}
|
|
471
|
+
if (!original.length) {
|
|
472
|
+
return {
|
|
473
|
+
rows: original,
|
|
474
|
+
metrics: {
|
|
475
|
+
...pluginMetrics(plugin.manifest, original),
|
|
476
|
+
status: 'skipped',
|
|
477
|
+
reason: 'no-candidates',
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const maxCandidates = normalizeInteger(
|
|
483
|
+
requestedMaxCandidates,
|
|
484
|
+
EVIDENCE_EMBEDDING_DEFAULT_MAX_CANDIDATES,
|
|
485
|
+
{ max: EVIDENCE_EMBEDDING_MAX_CANDIDATES },
|
|
486
|
+
);
|
|
487
|
+
const maxInputBytes = normalizeInteger(
|
|
488
|
+
requestedMaxInputBytes,
|
|
489
|
+
EVIDENCE_EMBEDDING_DEFAULT_MAX_INPUT_BYTES,
|
|
490
|
+
{ max: EVIDENCE_EMBEDDING_MAX_INPUT_BYTES },
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
let selection;
|
|
494
|
+
try {
|
|
495
|
+
selection = selectDocuments(original, String(query), plugin.manifest, {
|
|
496
|
+
maxCandidates,
|
|
497
|
+
maxInputBytes,
|
|
498
|
+
});
|
|
499
|
+
if (signal?.aborted) {
|
|
500
|
+
throw new EvidenceEmbeddingExecutionError('evidence embedding request was aborted');
|
|
501
|
+
}
|
|
502
|
+
const response = await plugin.embed({
|
|
503
|
+
schema_version: 1,
|
|
504
|
+
model_fingerprint: plugin.manifest.model_fingerprint,
|
|
505
|
+
query: { text: String(query) },
|
|
506
|
+
documents: selection.documents.map(({ id, text }) => ({ id, text })),
|
|
507
|
+
}, { signal });
|
|
508
|
+
const validated = validateResponse(response, plugin.manifest, selection.documents);
|
|
509
|
+
const scored = selection.documents.map((document) => ({
|
|
510
|
+
document,
|
|
511
|
+
similarity: cosine(validated.queryVector, validated.vectors.get(document.id)),
|
|
512
|
+
})).sort((left, right) => right.similarity - left.similarity
|
|
513
|
+
|| left.document.original_index - right.document.original_index
|
|
514
|
+
|| compareText(left.document.id, right.document.id));
|
|
515
|
+
const embeddedIds = new Set(selection.documents.map((document) => document.id));
|
|
516
|
+
const trailing = original.filter((row) => !embeddedIds.has(String(row?.chunk_id || '')));
|
|
517
|
+
const metrics = pluginMetrics(plugin.manifest, original, selection);
|
|
518
|
+
metrics.scores = scored.map(({ document, similarity }) => ({
|
|
519
|
+
chunk_id: document.id,
|
|
520
|
+
similarity,
|
|
521
|
+
}));
|
|
522
|
+
return {
|
|
523
|
+
rows: [...scored.map(({ document }) => document.row), ...trailing],
|
|
524
|
+
metrics,
|
|
525
|
+
};
|
|
526
|
+
} catch (rawError) {
|
|
527
|
+
const error = normalizePluginError(rawError);
|
|
528
|
+
if (required) throw error;
|
|
529
|
+
return fallback(original, plugin, error);
|
|
530
|
+
}
|
|
531
|
+
}
|