@agenr/agenr-plugin 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-7WL5EAQZ.js +758 -0
- package/dist/index.js +2538 -16537
- package/openclaw.plugin.json +8 -2
- package/package.json +3 -3
- package/dist/anthropic-RE4XNAKE.js +0 -5515
- package/dist/azure-openai-responses-IQLXOCZS.js +0 -190
- package/dist/chunk-6DQXEU2A.js +0 -32306
- package/dist/chunk-EAQYK3U2.js +0 -41
- package/dist/chunk-HNWLZUWE.js +0 -31
- package/dist/chunk-JRUUYSFL.js +0 -262
- package/dist/chunk-OLOUBEE5.js +0 -14022
- package/dist/chunk-P5HNPYGQ.js +0 -174
- package/dist/chunk-RD7BUOBD.js +0 -416
- package/dist/chunk-RWWH2U4W.js +0 -7056
- package/dist/chunk-SEOMNQGB.js +0 -86
- package/dist/chunk-SQLXP7LT.js +0 -4792
- package/dist/chunk-URGOKODJ.js +0 -17
- package/dist/dist-R6ESEJ6P.js +0 -1244
- package/dist/google-NAVXTQLO.js +0 -371
- package/dist/google-gemini-cli-NKYJWHX2.js +0 -712
- package/dist/google-vertex-ZBJ2EDRH.js +0 -414
- package/dist/mistral-SBQYC4J5.js +0 -38407
- package/dist/multipart-parser-DV373IRF.js +0 -371
- package/dist/openai-codex-responses-XN3T3DEN.js +0 -712
- package/dist/openai-completions-75ZFOFU6.js +0 -657
- package/dist/openai-responses-DCK4BVNT.js +0 -198
- package/dist/src-T5RRS2HN.js +0 -1408
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
// src/core/recall/scoring.ts
|
|
2
|
+
var DAY_IN_MILLISECONDS = 1e3 * 60 * 60 * 24;
|
|
3
|
+
var IMPORTANCE_FLOOR = 0.4;
|
|
4
|
+
var RELEVANCE_WEIGHT = 0.5;
|
|
5
|
+
var RECENCY_WEIGHT = 0.25;
|
|
6
|
+
var IMPORTANCE_WEIGHT = 0.25;
|
|
7
|
+
function recencyScore(createdAt, expiry, now = /* @__PURE__ */ new Date()) {
|
|
8
|
+
if (expiry === "core") {
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
const createdDate = asValidDate(createdAt);
|
|
12
|
+
const nowDate = asValidDate(now);
|
|
13
|
+
if (!createdDate || !nowDate) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
const halfLifeDays = expiry === "permanent" ? 365 : 30;
|
|
17
|
+
const daysOld = Math.max(0, (nowDate.getTime() - createdDate.getTime()) / DAY_IN_MILLISECONDS);
|
|
18
|
+
return clampUnit(Math.pow(0.5, daysOld / halfLifeDays));
|
|
19
|
+
}
|
|
20
|
+
function gaussianRecency(createdAt, aroundDate, radiusDays) {
|
|
21
|
+
const createdDate = asValidDate(createdAt);
|
|
22
|
+
const anchorDate = asValidDate(aroundDate);
|
|
23
|
+
const normalizedRadius = sanitizeNonNegative(radiusDays);
|
|
24
|
+
if (!createdDate || !anchorDate) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
if (normalizedRadius <= 0) {
|
|
28
|
+
return createdDate.getTime() === anchorDate.getTime() ? 1 : 0;
|
|
29
|
+
}
|
|
30
|
+
const daysDelta = Math.abs(createdDate.getTime() - anchorDate.getTime()) / DAY_IN_MILLISECONDS;
|
|
31
|
+
return clampUnit(Math.exp(-0.5 * (daysDelta / normalizedRadius) ** 2));
|
|
32
|
+
}
|
|
33
|
+
function importanceScore(importance) {
|
|
34
|
+
const clampedImportance = clampRange(sanitizeNonNegative(importance), 1, 10);
|
|
35
|
+
return clampUnit(IMPORTANCE_FLOOR + (clampedImportance - 1) / 9 * (1 - IMPORTANCE_FLOOR));
|
|
36
|
+
}
|
|
37
|
+
function combinedRelevance(vectorSim, lexical) {
|
|
38
|
+
const normalizedVector = clampUnit(sanitizeNonNegative(vectorSim));
|
|
39
|
+
const normalizedLexical = clampUnit(sanitizeNonNegative(lexical));
|
|
40
|
+
if (normalizedVector > 0 && normalizedLexical > 0) {
|
|
41
|
+
return clampUnit(normalizedVector * 0.6 + normalizedLexical * 0.4);
|
|
42
|
+
}
|
|
43
|
+
return Math.max(normalizedVector, normalizedLexical);
|
|
44
|
+
}
|
|
45
|
+
function scoreCandidate(params) {
|
|
46
|
+
const vector = clampUnit(sanitizeNonNegative(params.vectorSim));
|
|
47
|
+
const lexical = clampUnit(sanitizeNonNegative(params.lexical));
|
|
48
|
+
const recency = clampUnit(sanitizeNonNegative(params.recency));
|
|
49
|
+
const importance = clampUnit(sanitizeNonNegative(params.importance));
|
|
50
|
+
const relevance = combinedRelevance(vector, lexical);
|
|
51
|
+
const score = clampUnit(relevance * RELEVANCE_WEIGHT + recency * RECENCY_WEIGHT + importance * IMPORTANCE_WEIGHT);
|
|
52
|
+
return {
|
|
53
|
+
score,
|
|
54
|
+
scores: {
|
|
55
|
+
relevance,
|
|
56
|
+
vector,
|
|
57
|
+
lexical,
|
|
58
|
+
recency,
|
|
59
|
+
importance
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function cosineSimilarity(left, right) {
|
|
64
|
+
const size = Math.min(left.length, right.length);
|
|
65
|
+
if (size === 0) {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
let dot = 0;
|
|
69
|
+
let leftNorm = 0;
|
|
70
|
+
let rightNorm = 0;
|
|
71
|
+
for (let index = 0; index < size; index += 1) {
|
|
72
|
+
const leftValue = sanitizeFinite(left[index]);
|
|
73
|
+
const rightValue = sanitizeFinite(right[index]);
|
|
74
|
+
dot += leftValue * rightValue;
|
|
75
|
+
leftNorm += leftValue * leftValue;
|
|
76
|
+
rightNorm += rightValue * rightValue;
|
|
77
|
+
}
|
|
78
|
+
if (leftNorm <= 0 || rightNorm <= 0) {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
return clampUnit(dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm)));
|
|
82
|
+
}
|
|
83
|
+
var asValidDate = (value) => {
|
|
84
|
+
const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
85
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
86
|
+
};
|
|
87
|
+
var clampUnit = (value) => clampRange(sanitizeNonNegative(value), 0, 1);
|
|
88
|
+
var clampRange = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
89
|
+
var sanitizeFinite = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
90
|
+
var sanitizeNonNegative = (value) => Math.max(0, sanitizeFinite(value));
|
|
91
|
+
|
|
92
|
+
// src/core/recall/temporal.ts
|
|
93
|
+
var DAY_IN_MILLISECONDS2 = 1e3 * 60 * 60 * 24;
|
|
94
|
+
var MONTH_INDEX = /* @__PURE__ */ new Map([
|
|
95
|
+
["january", 0],
|
|
96
|
+
["february", 1],
|
|
97
|
+
["march", 2],
|
|
98
|
+
["april", 3],
|
|
99
|
+
["may", 4],
|
|
100
|
+
["june", 5],
|
|
101
|
+
["july", 6],
|
|
102
|
+
["august", 7],
|
|
103
|
+
["september", 8],
|
|
104
|
+
["october", 9],
|
|
105
|
+
["november", 10],
|
|
106
|
+
["december", 11]
|
|
107
|
+
]);
|
|
108
|
+
function inferAroundDate(text, now = /* @__PURE__ */ new Date()) {
|
|
109
|
+
const normalized = text.trim().toLowerCase();
|
|
110
|
+
const referenceNow = asValidDate2(now);
|
|
111
|
+
if (normalized.length === 0 || !referenceNow) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
let inferred = null;
|
|
115
|
+
if (/\byesterday\b/.test(normalized)) {
|
|
116
|
+
inferred = offsetDays(referenceNow, 1);
|
|
117
|
+
} else if (/\blast week\b/.test(normalized)) {
|
|
118
|
+
inferred = offsetDays(referenceNow, 7);
|
|
119
|
+
} else if (/\blast month\b/.test(normalized)) {
|
|
120
|
+
inferred = offsetDays(referenceNow, 30);
|
|
121
|
+
} else if (/\blast year\b/.test(normalized)) {
|
|
122
|
+
inferred = offsetDays(referenceNow, 365);
|
|
123
|
+
} else if (/\bthis week\b/.test(normalized)) {
|
|
124
|
+
inferred = offsetDays(referenceNow, 3);
|
|
125
|
+
} else if (/\bthis month\b/.test(normalized)) {
|
|
126
|
+
inferred = offsetDays(referenceNow, 15);
|
|
127
|
+
} else {
|
|
128
|
+
const relativeMatch = normalized.match(/\b(\d+)\s+(day|days|week|weeks|month|months)\s+ago\b/);
|
|
129
|
+
if (relativeMatch) {
|
|
130
|
+
const amount = Number(relativeMatch[1]);
|
|
131
|
+
const unit = relativeMatch[2];
|
|
132
|
+
const multiplier = unit?.startsWith("week") ? 7 : unit?.startsWith("month") ? 30 : 1;
|
|
133
|
+
inferred = Number.isFinite(amount) ? offsetDays(referenceNow, amount * multiplier) : null;
|
|
134
|
+
} else {
|
|
135
|
+
const monthMatch = normalized.match(/\bin\s+(january|february|march|april|may|june|july|august|september|october|november|december)\b/);
|
|
136
|
+
if (monthMatch?.[1]) {
|
|
137
|
+
inferred = inferMonthAnchor(monthMatch[1], referenceNow);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!inferred) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return inferred.getTime() > referenceNow.getTime() ? new Date(referenceNow.getTime()) : inferred;
|
|
145
|
+
}
|
|
146
|
+
function parseRelativeDate(input, now = /* @__PURE__ */ new Date()) {
|
|
147
|
+
const trimmed = input.trim();
|
|
148
|
+
const referenceNow = asValidDate2(now);
|
|
149
|
+
if (trimmed.length === 0 || !referenceNow) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const durationMatch = trimmed.match(/^(\d+)d$/i);
|
|
153
|
+
if (durationMatch?.[1]) {
|
|
154
|
+
const days = Number(durationMatch[1]);
|
|
155
|
+
return Number.isFinite(days) ? offsetDays(referenceNow, days) : null;
|
|
156
|
+
}
|
|
157
|
+
const parsed = new Date(trimmed);
|
|
158
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
159
|
+
}
|
|
160
|
+
var asValidDate2 = (value) => {
|
|
161
|
+
const date = new Date(value.getTime());
|
|
162
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
163
|
+
};
|
|
164
|
+
var offsetDays = (date, days) => new Date(date.getTime() - days * DAY_IN_MILLISECONDS2);
|
|
165
|
+
var inferMonthAnchor = (monthName, now) => {
|
|
166
|
+
const monthIndex = MONTH_INDEX.get(monthName);
|
|
167
|
+
if (monthIndex === void 0) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const year = monthIndex <= now.getUTCMonth() ? now.getUTCFullYear() : now.getUTCFullYear() - 1;
|
|
171
|
+
return new Date(Date.UTC(year, monthIndex, 15));
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/core/recall/lexical.ts
|
|
175
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
176
|
+
"the",
|
|
177
|
+
"a",
|
|
178
|
+
"an",
|
|
179
|
+
"is",
|
|
180
|
+
"are",
|
|
181
|
+
"was",
|
|
182
|
+
"were",
|
|
183
|
+
"be",
|
|
184
|
+
"been",
|
|
185
|
+
"being",
|
|
186
|
+
"have",
|
|
187
|
+
"has",
|
|
188
|
+
"had",
|
|
189
|
+
"do",
|
|
190
|
+
"does",
|
|
191
|
+
"did",
|
|
192
|
+
"will",
|
|
193
|
+
"would",
|
|
194
|
+
"could",
|
|
195
|
+
"should",
|
|
196
|
+
"may",
|
|
197
|
+
"might",
|
|
198
|
+
"shall",
|
|
199
|
+
"can",
|
|
200
|
+
"need",
|
|
201
|
+
"must",
|
|
202
|
+
"i",
|
|
203
|
+
"me",
|
|
204
|
+
"my",
|
|
205
|
+
"we",
|
|
206
|
+
"our",
|
|
207
|
+
"you",
|
|
208
|
+
"your",
|
|
209
|
+
"he",
|
|
210
|
+
"him",
|
|
211
|
+
"his",
|
|
212
|
+
"she",
|
|
213
|
+
"her",
|
|
214
|
+
"it",
|
|
215
|
+
"its",
|
|
216
|
+
"they",
|
|
217
|
+
"them",
|
|
218
|
+
"their",
|
|
219
|
+
"this",
|
|
220
|
+
"that",
|
|
221
|
+
"these",
|
|
222
|
+
"those",
|
|
223
|
+
"what",
|
|
224
|
+
"which",
|
|
225
|
+
"who",
|
|
226
|
+
"whom",
|
|
227
|
+
"in",
|
|
228
|
+
"on",
|
|
229
|
+
"at",
|
|
230
|
+
"to",
|
|
231
|
+
"for",
|
|
232
|
+
"of",
|
|
233
|
+
"with",
|
|
234
|
+
"by",
|
|
235
|
+
"from",
|
|
236
|
+
"up",
|
|
237
|
+
"about",
|
|
238
|
+
"into",
|
|
239
|
+
"through",
|
|
240
|
+
"during",
|
|
241
|
+
"before",
|
|
242
|
+
"after",
|
|
243
|
+
"above",
|
|
244
|
+
"below",
|
|
245
|
+
"and",
|
|
246
|
+
"or",
|
|
247
|
+
"but",
|
|
248
|
+
"not",
|
|
249
|
+
"no",
|
|
250
|
+
"nor",
|
|
251
|
+
"so",
|
|
252
|
+
"if",
|
|
253
|
+
"then",
|
|
254
|
+
"else",
|
|
255
|
+
"when",
|
|
256
|
+
"where",
|
|
257
|
+
"how",
|
|
258
|
+
"all",
|
|
259
|
+
"each",
|
|
260
|
+
"every",
|
|
261
|
+
"both",
|
|
262
|
+
"few",
|
|
263
|
+
"more",
|
|
264
|
+
"some",
|
|
265
|
+
"any",
|
|
266
|
+
"other",
|
|
267
|
+
"than"
|
|
268
|
+
]);
|
|
269
|
+
var FTS_OPERATOR_TOKENS = /* @__PURE__ */ new Set(["or", "not", "near"]);
|
|
270
|
+
function tokenize(text) {
|
|
271
|
+
const matches = text.toLowerCase().match(/[a-z0-9][a-z0-9._-]*/g) ?? [];
|
|
272
|
+
return matches.filter((token) => token.length >= 2 && !STOP_WORDS.has(token));
|
|
273
|
+
}
|
|
274
|
+
function buildLexicalPlan(text) {
|
|
275
|
+
const trimmed = text.trim();
|
|
276
|
+
if (trimmed.length === 0) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const tokens = tokenize(trimmed).filter((token) => !FTS_OPERATOR_TOKENS.has(token));
|
|
280
|
+
if (tokens.length === 0) {
|
|
281
|
+
return [
|
|
282
|
+
{
|
|
283
|
+
tier: "exact",
|
|
284
|
+
text: trimmed
|
|
285
|
+
}
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
if (tokens.length === 1) {
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
tier: "exact",
|
|
292
|
+
text: trimmed
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
tier: "all_tokens",
|
|
296
|
+
tokens
|
|
297
|
+
}
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
tier: "exact",
|
|
303
|
+
text: trimmed
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
tier: "all_tokens",
|
|
307
|
+
tokens
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
tier: "any_tokens",
|
|
311
|
+
tokens
|
|
312
|
+
}
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
function computeLexicalScore(query, subject, content) {
|
|
316
|
+
const queryTokens = tokenize(query);
|
|
317
|
+
const subjectTokens = tokenize(subject);
|
|
318
|
+
const contentTokens = tokenize(content);
|
|
319
|
+
const subjectTokenSet = new Set(subjectTokens);
|
|
320
|
+
const contentTokenSet = new Set(contentTokens);
|
|
321
|
+
const tokenOverlap = queryTokens.length === 0 ? 0 : queryTokens.filter((token) => subjectTokenSet.has(token) || contentTokenSet.has(token)).length / queryTokens.length;
|
|
322
|
+
const phraseMatches = countPhraseMatches(queryTokens, subjectTokens, contentTokens);
|
|
323
|
+
const phraseBonus = Math.min(0.4, phraseMatches * 0.2);
|
|
324
|
+
const subjectBonus = normalizeText(query) === normalizeText(subject) && normalizeText(query).length > 0 ? 0.3 : 0;
|
|
325
|
+
return Math.min(1, tokenOverlap + phraseBonus + subjectBonus);
|
|
326
|
+
}
|
|
327
|
+
var normalizeText = (text) => text.trim().toLowerCase();
|
|
328
|
+
var countPhraseMatches = (queryTokens, subjectTokens, contentTokens) => {
|
|
329
|
+
if (queryTokens.length < 2) {
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
const matchedPhrases = /* @__PURE__ */ new Set();
|
|
333
|
+
for (let size = 2; size <= queryTokens.length; size += 1) {
|
|
334
|
+
for (let index = 0; index + size <= queryTokens.length; index += 1) {
|
|
335
|
+
const phraseTokens = queryTokens.slice(index, index + size);
|
|
336
|
+
if (hasConsecutivePhrase(subjectTokens, phraseTokens) || hasConsecutivePhrase(contentTokens, phraseTokens)) {
|
|
337
|
+
matchedPhrases.add(phraseTokens.join(" "));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return matchedPhrases.size;
|
|
342
|
+
};
|
|
343
|
+
var hasConsecutivePhrase = (haystack, needle) => {
|
|
344
|
+
if (needle.length === 0 || haystack.length < needle.length) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
for (let index = 0; index + needle.length <= haystack.length; index += 1) {
|
|
348
|
+
let matches = true;
|
|
349
|
+
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
350
|
+
if (haystack[index + offset] !== needle[offset]) {
|
|
351
|
+
matches = false;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (matches) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/core/recall/trace.ts
|
|
363
|
+
var NOOP_RECALL_TRACE_SINK = {
|
|
364
|
+
reportSummary() {
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
function createNoopRecallTraceSink() {
|
|
368
|
+
return NOOP_RECALL_TRACE_SINK;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/core/recall/search.ts
|
|
372
|
+
var MIN_VECTOR_ONLY_EVIDENCE = 0.3;
|
|
373
|
+
var HISTORICAL_STATE_FLAT_RECENCY = 0.5;
|
|
374
|
+
var HISTORICAL_PREDECESSOR_BOOST = 0.08;
|
|
375
|
+
var HISTORICAL_RETIRED_PREDECESSOR_BOOST = 0.06;
|
|
376
|
+
var HISTORICAL_OLDER_STATE_BOOST = 0.08;
|
|
377
|
+
var HISTORICAL_TOPIC_SHARED_PREFIX_MIN = 2;
|
|
378
|
+
var HISTORICAL_TOPIC_PREFIX_OF_CANDIDATE_MIN = 0.6;
|
|
379
|
+
async function recall(query, ports, options = {}) {
|
|
380
|
+
const text = query.text.trim();
|
|
381
|
+
const limit = normalizeLimit(query.limit);
|
|
382
|
+
const threshold = normalizeThreshold(query.threshold);
|
|
383
|
+
const budget = normalizeBudget(query.budget);
|
|
384
|
+
const aroundDate = query.around !== void 0 ? parseAroundDate(query.around) : inferAroundDate(text);
|
|
385
|
+
const since = query.since ? parseRelativeDate(query.since) : null;
|
|
386
|
+
const until = query.until ? parseRelativeDate(query.until) : null;
|
|
387
|
+
const filters = buildEntryFilters(query.types, query.tags, since, until);
|
|
388
|
+
const trace = options.trace ?? createNoopRecallTraceSink();
|
|
389
|
+
const summary = buildRecallTraceSummary({
|
|
390
|
+
filters,
|
|
391
|
+
limit,
|
|
392
|
+
threshold,
|
|
393
|
+
budget,
|
|
394
|
+
aroundDate,
|
|
395
|
+
aroundSource: query.around !== void 0 ? "explicit" : "inferred",
|
|
396
|
+
aroundRadius: aroundDate ? normalizeAroundRadius(query.aroundRadius) : void 0
|
|
397
|
+
});
|
|
398
|
+
let traceReported = false;
|
|
399
|
+
const reportTrace = (noResultReason) => {
|
|
400
|
+
if (traceReported) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
traceReported = true;
|
|
404
|
+
finishRecallTrace(summary, trace, noResultReason);
|
|
405
|
+
};
|
|
406
|
+
if (text.length === 0) {
|
|
407
|
+
reportTrace("empty_query");
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
if (limit === 0) {
|
|
411
|
+
reportTrace("limit_zero");
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
const queryEmbedding = await ports.embed(text);
|
|
416
|
+
const [vectorCandidates, ftsCandidates] = await Promise.all([
|
|
417
|
+
ports.vectorSearch({
|
|
418
|
+
embedding: queryEmbedding,
|
|
419
|
+
limit: limit * 4,
|
|
420
|
+
filters
|
|
421
|
+
}),
|
|
422
|
+
ports.ftsSearch({
|
|
423
|
+
text,
|
|
424
|
+
limit: limit * 2,
|
|
425
|
+
filters
|
|
426
|
+
})
|
|
427
|
+
]);
|
|
428
|
+
const mergeStartedAt = Date.now();
|
|
429
|
+
const mergedCandidates = mergeCandidates(vectorCandidates, ftsCandidates);
|
|
430
|
+
await expandHistoricalCandidates(mergedCandidates, queryEmbedding, ports, {
|
|
431
|
+
activeEntryIds: Array.from(mergedCandidates.keys()),
|
|
432
|
+
rankingProfile: query.rankingProfile
|
|
433
|
+
});
|
|
434
|
+
summary.candidateCounts.merged = mergedCandidates.size;
|
|
435
|
+
summary.timings.mergeCandidatesMs = elapsedMs(mergeStartedAt);
|
|
436
|
+
const scoreStartedAt = Date.now();
|
|
437
|
+
const scored = applyHistoricalLineageBoosts(
|
|
438
|
+
Array.from(mergedCandidates.values()).map(
|
|
439
|
+
(candidate) => scoreMergedCandidate(candidate, text, queryEmbedding, {
|
|
440
|
+
aroundDate,
|
|
441
|
+
aroundRadius: query.aroundRadius,
|
|
442
|
+
rankingProfile: query.rankingProfile
|
|
443
|
+
})
|
|
444
|
+
),
|
|
445
|
+
{
|
|
446
|
+
aroundDate,
|
|
447
|
+
rankingProfile: query.rankingProfile
|
|
448
|
+
}
|
|
449
|
+
).sort((left, right) => right.score - left.score);
|
|
450
|
+
summary.timings.scoreCandidatesMs = elapsedMs(scoreStartedAt);
|
|
451
|
+
const thresholdStartedAt = Date.now();
|
|
452
|
+
const thresholded = scored.filter((result) => hasSufficientReturnEvidence(result) && result.score >= threshold);
|
|
453
|
+
summary.candidateCounts.thresholdQualified = thresholded.length;
|
|
454
|
+
summary.timings.thresholdMs = elapsedMs(thresholdStartedAt);
|
|
455
|
+
if (thresholded.length === 0) {
|
|
456
|
+
reportTrace(scored.length === 0 ? "no_candidates" : "below_threshold");
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
const budgetStartedAt = Date.now();
|
|
460
|
+
const budgeted = budget === null ? thresholded : applyBudget(thresholded, budget);
|
|
461
|
+
summary.candidateCounts.budgetAccepted = budgeted.length;
|
|
462
|
+
summary.timings.budgetMs = budget === null ? 0 : elapsedMs(budgetStartedAt);
|
|
463
|
+
const ranked = budgeted.slice(0, limit);
|
|
464
|
+
summary.candidateCounts.finalRanked = ranked.length;
|
|
465
|
+
if (ranked.length === 0) {
|
|
466
|
+
reportTrace("limit_zero");
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
const hydratedEntries = await ports.hydrateEntries(ranked.map((result) => result.entry.id));
|
|
470
|
+
const shapeStartedAt = Date.now();
|
|
471
|
+
const hydratedById = new Map(hydratedEntries.map((entry) => [entry.id, entry]));
|
|
472
|
+
const results = ranked.flatMap((result) => {
|
|
473
|
+
const entry = hydratedById.get(result.entry.id);
|
|
474
|
+
if (!entry) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
return [
|
|
478
|
+
{
|
|
479
|
+
entry,
|
|
480
|
+
score: result.score,
|
|
481
|
+
scores: result.scores
|
|
482
|
+
}
|
|
483
|
+
];
|
|
484
|
+
});
|
|
485
|
+
summary.candidateCounts.returned = results.length;
|
|
486
|
+
summary.timings.shapeResultsMs = elapsedMs(shapeStartedAt);
|
|
487
|
+
if (results.length === 0) {
|
|
488
|
+
reportTrace("hydrate_missing");
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
if (results.length > 0) {
|
|
492
|
+
await ports.recordRecallEvents({
|
|
493
|
+
entryIds: results.map((result) => result.entry.id),
|
|
494
|
+
query: text,
|
|
495
|
+
sessionKey: query.sessionKey
|
|
496
|
+
}).catch(() => void 0);
|
|
497
|
+
}
|
|
498
|
+
reportTrace();
|
|
499
|
+
return results;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
reportTrace();
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function buildRecallTraceSummary(params) {
|
|
506
|
+
return {
|
|
507
|
+
filtering: {
|
|
508
|
+
types: params.filters?.types ?? [],
|
|
509
|
+
tags: params.filters?.tags ?? [],
|
|
510
|
+
since: params.filters?.since?.toISOString(),
|
|
511
|
+
until: params.filters?.until?.toISOString(),
|
|
512
|
+
around: params.aroundDate ? {
|
|
513
|
+
source: params.aroundSource,
|
|
514
|
+
anchor: params.aroundDate.toISOString(),
|
|
515
|
+
radiusDays: params.aroundRadius ?? 14
|
|
516
|
+
} : void 0
|
|
517
|
+
},
|
|
518
|
+
ranking: {
|
|
519
|
+
limit: params.limit,
|
|
520
|
+
threshold: params.threshold,
|
|
521
|
+
budget: params.budget
|
|
522
|
+
},
|
|
523
|
+
candidateCounts: {
|
|
524
|
+
merged: 0,
|
|
525
|
+
thresholdQualified: 0,
|
|
526
|
+
budgetAccepted: 0,
|
|
527
|
+
finalRanked: 0,
|
|
528
|
+
returned: 0
|
|
529
|
+
},
|
|
530
|
+
timings: {
|
|
531
|
+
mergeCandidatesMs: 0,
|
|
532
|
+
scoreCandidatesMs: 0,
|
|
533
|
+
thresholdMs: 0,
|
|
534
|
+
budgetMs: 0,
|
|
535
|
+
shapeResultsMs: 0
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function finishRecallTrace(summary, trace, noResultReason) {
|
|
540
|
+
if (noResultReason) {
|
|
541
|
+
summary.ranking.noResultReason = noResultReason;
|
|
542
|
+
}
|
|
543
|
+
trace.reportSummary(summary);
|
|
544
|
+
}
|
|
545
|
+
function scoreMergedCandidate(candidate, queryText, queryEmbedding, params) {
|
|
546
|
+
const vector = candidate.vectorSim ?? cosineSimilarity(candidate.entry.embedding ?? [], queryEmbedding);
|
|
547
|
+
const lexical = computeLexicalScore(queryText, candidate.entry.subject, candidate.entry.content);
|
|
548
|
+
const recency = resolveRecencyScore(candidate.entry, params);
|
|
549
|
+
const importance = importanceScore(candidate.entry.importance);
|
|
550
|
+
const scored = scoreCandidate({
|
|
551
|
+
vectorSim: vector,
|
|
552
|
+
lexical,
|
|
553
|
+
recency,
|
|
554
|
+
importance
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
entry: candidate.entry,
|
|
558
|
+
score: scored.score,
|
|
559
|
+
scores: scored.scores
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
async function expandHistoricalCandidates(mergedCandidates, queryEmbedding, ports, params) {
|
|
563
|
+
if (params.rankingProfile !== "historical_state" || mergedCandidates.size === 0 || !ports.fetchPredecessors) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const predecessors = await ports.fetchPredecessors({
|
|
567
|
+
activeEntryIds: params.activeEntryIds
|
|
568
|
+
});
|
|
569
|
+
for (const entry of predecessors) {
|
|
570
|
+
if (mergedCandidates.has(entry.id)) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
mergedCandidates.set(entry.id, {
|
|
574
|
+
entry,
|
|
575
|
+
vectorSim: cosineSimilarity(entry.embedding ?? [], queryEmbedding)
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function resolveRecencyScore(entry, params) {
|
|
580
|
+
if (params.aroundDate) {
|
|
581
|
+
return gaussianRecency(entry.created_at, params.aroundDate, normalizeAroundRadius(params.aroundRadius));
|
|
582
|
+
}
|
|
583
|
+
if (params.rankingProfile === "historical_state") {
|
|
584
|
+
return HISTORICAL_STATE_FLAT_RECENCY;
|
|
585
|
+
}
|
|
586
|
+
return recencyScore(entry.created_at, entry.expiry);
|
|
587
|
+
}
|
|
588
|
+
function applyHistoricalLineageBoosts(candidates, params) {
|
|
589
|
+
if (params.rankingProfile !== "historical_state") {
|
|
590
|
+
return candidates;
|
|
591
|
+
}
|
|
592
|
+
const entries = candidates.map((candidate) => candidate.entry);
|
|
593
|
+
return candidates.map((candidate) => {
|
|
594
|
+
const bonus = resolveHistoricalLineageBonus(candidate.entry, entries, params.aroundDate);
|
|
595
|
+
if (bonus <= 0) {
|
|
596
|
+
return candidate;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
...candidate,
|
|
600
|
+
score: Math.min(1, candidate.score + bonus)
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function resolveHistoricalLineageBonus(entry, entries, aroundDate) {
|
|
605
|
+
if (entries.some((peer) => peer.id !== entry.id && entry.superseded_by === peer.id)) {
|
|
606
|
+
return HISTORICAL_PREDECESSOR_BOOST;
|
|
607
|
+
}
|
|
608
|
+
if (aroundDate) {
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
611
|
+
const activePeers = entries.filter((peer) => peer.id !== entry.id && isPotentialCurrentPeer(peer) && isOlderHistoricalPeer(entry, peer));
|
|
612
|
+
if (activePeers.length === 0) {
|
|
613
|
+
return 0;
|
|
614
|
+
}
|
|
615
|
+
return entry.retired ? HISTORICAL_RETIRED_PREDECESSOR_BOOST : HISTORICAL_OLDER_STATE_BOOST;
|
|
616
|
+
}
|
|
617
|
+
function isPotentialCurrentPeer(entry) {
|
|
618
|
+
return !entry.retired && entry.superseded_by === void 0;
|
|
619
|
+
}
|
|
620
|
+
function isOlderHistoricalPeer(left, right) {
|
|
621
|
+
return createdAtMs(left.created_at) < createdAtMs(right.created_at) && sharesHistoricalLineage(left, right);
|
|
622
|
+
}
|
|
623
|
+
function sharesHistoricalLineage(left, right) {
|
|
624
|
+
if (left.claim_key && right.claim_key && left.claim_key === right.claim_key) {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
return sharesHistoricalTopic(left, right);
|
|
628
|
+
}
|
|
629
|
+
function sharesHistoricalTopic(left, right) {
|
|
630
|
+
const leftTokens = tokenize(left.subject);
|
|
631
|
+
const rightTokens = tokenize(right.subject);
|
|
632
|
+
if (leftTokens.length === 0 || rightTokens.length === 0) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
const sharedPrefixCount = countSharedPrefixTokens(leftTokens, rightTokens);
|
|
636
|
+
return sharedPrefixCount >= HISTORICAL_TOPIC_SHARED_PREFIX_MIN && sharedPrefixCount / leftTokens.length >= HISTORICAL_TOPIC_PREFIX_OF_CANDIDATE_MIN;
|
|
637
|
+
}
|
|
638
|
+
function createdAtMs(value) {
|
|
639
|
+
const timestamp = new Date(value).getTime();
|
|
640
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
641
|
+
}
|
|
642
|
+
function countSharedPrefixTokens(leftTokens, rightTokens) {
|
|
643
|
+
const length = Math.min(leftTokens.length, rightTokens.length);
|
|
644
|
+
let sharedPrefixCount = 0;
|
|
645
|
+
for (let index = 0; index < length; index += 1) {
|
|
646
|
+
if (leftTokens[index] !== rightTokens[index]) {
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
sharedPrefixCount += 1;
|
|
650
|
+
}
|
|
651
|
+
return sharedPrefixCount;
|
|
652
|
+
}
|
|
653
|
+
function hasSufficientReturnEvidence(candidate) {
|
|
654
|
+
if (candidate.scores.lexical > 0) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
return candidate.scores.vector >= MIN_VECTOR_ONLY_EVIDENCE;
|
|
658
|
+
}
|
|
659
|
+
function mergeCandidates(vectorCandidates, ftsCandidates) {
|
|
660
|
+
const merged = /* @__PURE__ */ new Map();
|
|
661
|
+
for (const candidate of vectorCandidates) {
|
|
662
|
+
merged.set(candidate.entry.id, {
|
|
663
|
+
entry: candidate.entry,
|
|
664
|
+
vectorSim: candidate.vectorSim
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
for (const candidate of ftsCandidates) {
|
|
668
|
+
const existing = merged.get(candidate.entry.id);
|
|
669
|
+
if (existing) {
|
|
670
|
+
existing.entry = existing.entry.embedding ? existing.entry : candidate.entry;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
merged.set(candidate.entry.id, {
|
|
674
|
+
entry: candidate.entry
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
return merged;
|
|
678
|
+
}
|
|
679
|
+
function buildEntryFilters(types, tags, since, until) {
|
|
680
|
+
const filters = {};
|
|
681
|
+
if (types && types.length > 0) {
|
|
682
|
+
filters.types = types;
|
|
683
|
+
}
|
|
684
|
+
if (tags && tags.length > 0) {
|
|
685
|
+
filters.tags = tags;
|
|
686
|
+
}
|
|
687
|
+
if (since) {
|
|
688
|
+
filters.since = since;
|
|
689
|
+
}
|
|
690
|
+
if (until) {
|
|
691
|
+
filters.until = until;
|
|
692
|
+
}
|
|
693
|
+
return Object.keys(filters).length > 0 ? filters : void 0;
|
|
694
|
+
}
|
|
695
|
+
function applyBudget(results, budget) {
|
|
696
|
+
if (results.length === 0) {
|
|
697
|
+
return [];
|
|
698
|
+
}
|
|
699
|
+
const accepted = [results[0]];
|
|
700
|
+
let consumed = estimateTokens(results[0].entry);
|
|
701
|
+
for (const result of results.slice(1)) {
|
|
702
|
+
const estimate = estimateTokens(result.entry);
|
|
703
|
+
if (consumed + estimate > budget) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
accepted.push(result);
|
|
707
|
+
consumed += estimate;
|
|
708
|
+
}
|
|
709
|
+
return accepted;
|
|
710
|
+
}
|
|
711
|
+
function estimateTokens(entry) {
|
|
712
|
+
return (entry.subject.length + entry.content.length) / 4;
|
|
713
|
+
}
|
|
714
|
+
function parseAroundDate(value) {
|
|
715
|
+
return parseRelativeDate(value) ?? inferAroundDate(value);
|
|
716
|
+
}
|
|
717
|
+
function normalizeLimit(value) {
|
|
718
|
+
if (!Number.isFinite(value)) {
|
|
719
|
+
return 10;
|
|
720
|
+
}
|
|
721
|
+
return Math.max(0, Math.floor(value ?? 10));
|
|
722
|
+
}
|
|
723
|
+
function normalizeThreshold(value) {
|
|
724
|
+
if (!Number.isFinite(value)) {
|
|
725
|
+
return 0;
|
|
726
|
+
}
|
|
727
|
+
return Math.min(1, Math.max(0, value ?? 0));
|
|
728
|
+
}
|
|
729
|
+
function normalizeBudget(value) {
|
|
730
|
+
if (!Number.isFinite(value)) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
return Math.max(0, value ?? 0);
|
|
734
|
+
}
|
|
735
|
+
function normalizeAroundRadius(value) {
|
|
736
|
+
if (!Number.isFinite(value) || (value ?? 0) <= 0) {
|
|
737
|
+
return 14;
|
|
738
|
+
}
|
|
739
|
+
return value;
|
|
740
|
+
}
|
|
741
|
+
function elapsedMs(startedAt) {
|
|
742
|
+
return Math.max(0, Date.now() - startedAt);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export {
|
|
746
|
+
recencyScore,
|
|
747
|
+
gaussianRecency,
|
|
748
|
+
importanceScore,
|
|
749
|
+
combinedRelevance,
|
|
750
|
+
scoreCandidate,
|
|
751
|
+
cosineSimilarity,
|
|
752
|
+
inferAroundDate,
|
|
753
|
+
parseRelativeDate,
|
|
754
|
+
tokenize,
|
|
755
|
+
buildLexicalPlan,
|
|
756
|
+
computeLexicalScore,
|
|
757
|
+
recall
|
|
758
|
+
};
|