@agenr/agenr-plugin 1.7.4 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,839 +0,0 @@
1
- import {
2
- cosineSimilarity,
3
- recall
4
- } from "./chunk-7WL5EAQZ.js";
5
-
6
- // src/core/episode/temporal-window.ts
7
- var DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1e3;
8
- var DEFAULT_ANCHOR_RADIUS_DAYS = 3;
9
- var MONTH_INDEX = /* @__PURE__ */ new Map([
10
- ["january", 0],
11
- ["february", 1],
12
- ["march", 2],
13
- ["april", 3],
14
- ["may", 4],
15
- ["june", 5],
16
- ["july", 6],
17
- ["august", 7],
18
- ["september", 8],
19
- ["october", 9],
20
- ["november", 10],
21
- ["december", 11]
22
- ]);
23
- var WEEKDAY_INDEX = /* @__PURE__ */ new Map([
24
- ["sunday", 0],
25
- ["monday", 1],
26
- ["tuesday", 2],
27
- ["wednesday", 3],
28
- ["thursday", 4],
29
- ["friday", 5],
30
- ["saturday", 6]
31
- ]);
32
- function parseTemporalWindow(text, now = /* @__PURE__ */ new Date()) {
33
- const normalizedText = text.trim();
34
- const referenceNow = asValidDate(now);
35
- if (normalizedText.length === 0 || !referenceNow) {
36
- return null;
37
- }
38
- const timezone = getSystemTimeZone();
39
- const lower = normalizedText.toLowerCase();
40
- if (/\btoday\b/.test(lower)) {
41
- return buildResolvedWindow({
42
- window: {
43
- kind: "interval",
44
- start: startOfDayLocal(referenceNow),
45
- end: referenceNow,
46
- source: "inferred"
47
- },
48
- resolvedFrom: "today",
49
- timezone,
50
- now: referenceNow
51
- });
52
- }
53
- if (/\byesterday\b/.test(lower)) {
54
- const target = addDaysLocal(referenceNow, -1);
55
- return buildResolvedWindow({
56
- window: {
57
- kind: "interval",
58
- start: startOfDayLocal(target),
59
- end: endOfDayLocal(target),
60
- source: "inferred"
61
- },
62
- resolvedFrom: "yesterday",
63
- timezone,
64
- now: referenceNow
65
- });
66
- }
67
- const monthDayMatch = normalizedText.match(
68
- /\b(?:on\s+)?((january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{1,2})(?:st|nd|rd|th)?)\b/i
69
- );
70
- if (monthDayMatch?.[1] && monthDayMatch[2] && monthDayMatch[3]) {
71
- const targetDate = resolveMostRecentMonthDay(monthDayMatch[2].toLowerCase(), Number(monthDayMatch[3]), referenceNow);
72
- if (targetDate) {
73
- return buildResolvedWindow({
74
- window: {
75
- kind: "interval",
76
- start: startOfDayLocal(targetDate),
77
- end: endOfDayLocal(targetDate),
78
- source: "inferred"
79
- },
80
- resolvedFrom: monthDayMatch[1],
81
- timezone,
82
- now: referenceNow
83
- });
84
- }
85
- }
86
- const weekdayMatch = normalizedText.match(/\b(last\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday))\b/i);
87
- if (weekdayMatch?.[1] && weekdayMatch[2]) {
88
- const targetDate = resolveLastWeekday(weekdayMatch[2].toLowerCase(), referenceNow);
89
- if (targetDate) {
90
- return buildResolvedWindow({
91
- window: {
92
- kind: "interval",
93
- start: startOfDayLocal(targetDate),
94
- end: endOfDayLocal(targetDate),
95
- source: "inferred"
96
- },
97
- resolvedFrom: weekdayMatch[1],
98
- timezone,
99
- now: referenceNow
100
- });
101
- }
102
- }
103
- if (/\bthis week\b/.test(lower)) {
104
- return buildResolvedWindow({
105
- window: {
106
- kind: "interval",
107
- start: startOfWeekLocal(referenceNow),
108
- end: referenceNow,
109
- source: "inferred"
110
- },
111
- resolvedFrom: "this week",
112
- timezone,
113
- now: referenceNow
114
- });
115
- }
116
- if (/\blast week\b/.test(lower)) {
117
- const previousWeekDate = addDaysLocal(startOfWeekLocal(referenceNow), -1);
118
- const start = startOfWeekLocal(previousWeekDate);
119
- return buildResolvedWindow({
120
- window: {
121
- kind: "interval",
122
- start,
123
- end: endOfWeekLocal(previousWeekDate),
124
- source: "inferred"
125
- },
126
- resolvedFrom: "last week",
127
- timezone,
128
- now: referenceNow
129
- });
130
- }
131
- if (/\bthis month\b/.test(lower)) {
132
- return buildResolvedWindow({
133
- window: {
134
- kind: "interval",
135
- start: startOfMonthLocal(referenceNow),
136
- end: referenceNow,
137
- source: "inferred"
138
- },
139
- resolvedFrom: "this month",
140
- timezone,
141
- now: referenceNow
142
- });
143
- }
144
- if (/\blast month\b/.test(lower)) {
145
- const previousMonthDate = new Date(referenceNow.getFullYear(), referenceNow.getMonth() - 1, 1, 12);
146
- return buildResolvedWindow({
147
- window: {
148
- kind: "interval",
149
- start: startOfMonthLocal(previousMonthDate),
150
- end: endOfMonthLocal(previousMonthDate),
151
- source: "inferred"
152
- },
153
- resolvedFrom: "last month",
154
- timezone,
155
- now: referenceNow
156
- });
157
- }
158
- const relativeMatch = lower.match(/\b(\d+)\s+(day|days|week|weeks|month|months)\s+ago\b/);
159
- if (relativeMatch?.[1] && relativeMatch[2]) {
160
- const amount = Number(relativeMatch[1]);
161
- if (Number.isFinite(amount) && amount > 0) {
162
- const unit = relativeMatch[2];
163
- if (unit.startsWith("day")) {
164
- const target = addDaysLocal(referenceNow, -amount);
165
- return buildResolvedWindow({
166
- window: {
167
- kind: "interval",
168
- start: startOfDayLocal(target),
169
- end: endOfDayLocal(target),
170
- source: "inferred"
171
- },
172
- resolvedFrom: relativeMatch[0],
173
- timezone,
174
- now: referenceNow
175
- });
176
- }
177
- if (unit.startsWith("week")) {
178
- return buildResolvedWindow({
179
- window: {
180
- kind: "anchor",
181
- anchor: addDaysLocal(referenceNow, -amount * 7),
182
- radiusDays: DEFAULT_ANCHOR_RADIUS_DAYS,
183
- source: "inferred"
184
- },
185
- resolvedFrom: relativeMatch[0],
186
- timezone,
187
- now: referenceNow
188
- });
189
- }
190
- if (unit.startsWith("month")) {
191
- return buildResolvedWindow({
192
- window: {
193
- kind: "anchor",
194
- anchor: subtractCalendarMonths(referenceNow, amount),
195
- radiusDays: DEFAULT_ANCHOR_RADIUS_DAYS,
196
- source: "inferred"
197
- },
198
- resolvedFrom: relativeMatch[0],
199
- timezone,
200
- now: referenceNow
201
- });
202
- }
203
- }
204
- }
205
- const monthMatch = lower.match(/\bin\s+(january|february|march|april|may|june|july|august|september|october|november|december)\b/);
206
- if (monthMatch?.[1]) {
207
- const targetMonth = resolveMostRecentMonth(monthMatch[1], referenceNow);
208
- if (targetMonth) {
209
- return buildResolvedWindow({
210
- window: {
211
- kind: "interval",
212
- start: startOfMonthLocal(targetMonth),
213
- end: endOfMonthLocal(targetMonth),
214
- source: "inferred"
215
- },
216
- resolvedFrom: monthMatch[0],
217
- timezone,
218
- now: referenceNow
219
- });
220
- }
221
- }
222
- const isoDateMatch = normalizedText.match(/\b(\d{4}-\d{2}-\d{2})(?:[tT][0-9:.+-Zz]+)?\b/);
223
- if (isoDateMatch?.[1]) {
224
- const targetDate = parseIsoDateLocal(isoDateMatch[1]);
225
- if (targetDate) {
226
- return buildResolvedWindow({
227
- window: {
228
- kind: "interval",
229
- start: startOfDayLocal(targetDate),
230
- end: endOfDayLocal(targetDate),
231
- source: "inferred"
232
- },
233
- resolvedFrom: isoDateMatch[1],
234
- timezone,
235
- now: referenceNow
236
- });
237
- }
238
- }
239
- return null;
240
- }
241
- function resolveTemporalWindowBounds(window, now = /* @__PURE__ */ new Date()) {
242
- switch (window.kind) {
243
- case "interval":
244
- return window.start && window.end ? { start: window.start, end: window.end } : null;
245
- case "anchor":
246
- if (!window.anchor || window.radiusDays === void 0 || window.radiusDays < 0) {
247
- return null;
248
- }
249
- return {
250
- start: new Date(window.anchor.getTime() - Math.trunc(window.radiusDays) * DAY_IN_MILLISECONDS),
251
- end: new Date(window.anchor.getTime() + Math.trunc(window.radiusDays) * DAY_IN_MILLISECONDS)
252
- };
253
- case "open_end":
254
- return window.start ? { start: window.start, end: asValidDate(now) ?? /* @__PURE__ */ new Date() } : null;
255
- case "open_start":
256
- return null;
257
- default:
258
- return null;
259
- }
260
- }
261
- function getSystemTimeZone() {
262
- return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
263
- }
264
- function buildResolvedWindow(params) {
265
- const bounds = resolveTemporalWindowBounds(params.window, params.now);
266
- if (!bounds) {
267
- return null;
268
- }
269
- return {
270
- window: params.window,
271
- bounds,
272
- timezone: params.timezone,
273
- resolvedFrom: params.resolvedFrom
274
- };
275
- }
276
- function asValidDate(value) {
277
- const normalized = new Date(value.getTime());
278
- return Number.isNaN(normalized.getTime()) ? null : normalized;
279
- }
280
- function addDaysLocal(date, days) {
281
- return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
282
- }
283
- function startOfDayLocal(date) {
284
- return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
285
- }
286
- function endOfDayLocal(date) {
287
- return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
288
- }
289
- function startOfWeekLocal(date) {
290
- const weekStart = resolveWeekStartDay();
291
- const currentDay = date.getDay();
292
- const offset = (currentDay - weekStart + 7) % 7;
293
- return startOfDayLocal(addDaysLocal(date, -offset));
294
- }
295
- function endOfWeekLocal(date) {
296
- return endOfDayLocal(addDaysLocal(startOfWeekLocal(date), 6));
297
- }
298
- function startOfMonthLocal(date) {
299
- return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
300
- }
301
- function endOfMonthLocal(date) {
302
- return new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999);
303
- }
304
- function resolveMostRecentMonth(monthName, now) {
305
- const monthIndex = MONTH_INDEX.get(monthName);
306
- if (monthIndex === void 0) {
307
- return null;
308
- }
309
- const year = monthIndex <= now.getMonth() ? now.getFullYear() : now.getFullYear() - 1;
310
- return new Date(year, monthIndex, 15, 12, 0, 0, 0);
311
- }
312
- function resolveMostRecentMonthDay(monthName, day, now) {
313
- const monthIndex = MONTH_INDEX.get(monthName);
314
- if (monthIndex === void 0) {
315
- return null;
316
- }
317
- const currentYearCandidate = buildLocalDateAtNoon(now.getFullYear(), monthIndex, day);
318
- if (!currentYearCandidate) {
319
- return null;
320
- }
321
- if (startOfDayLocal(currentYearCandidate).getTime() <= startOfDayLocal(now).getTime()) {
322
- return currentYearCandidate;
323
- }
324
- return buildLocalDateAtNoon(now.getFullYear() - 1, monthIndex, day);
325
- }
326
- function parseIsoDateLocal(value) {
327
- const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
328
- if (!match?.[1] || !match[2] || !match[3]) {
329
- return null;
330
- }
331
- const year = Number(match[1]);
332
- const month = Number(match[2]) - 1;
333
- const day = Number(match[3]);
334
- return buildLocalDateAtNoon(year, month, day);
335
- }
336
- function subtractCalendarMonths(date, months) {
337
- const targetMonthIndex = date.getMonth() - months;
338
- const targetYear = date.getFullYear() + Math.floor(targetMonthIndex / 12);
339
- const normalizedMonth = (targetMonthIndex % 12 + 12) % 12;
340
- const targetLastDay = new Date(targetYear, normalizedMonth + 1, 0).getDate();
341
- const day = Math.min(date.getDate(), targetLastDay);
342
- return new Date(targetYear, normalizedMonth, day, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
343
- }
344
- function resolveLastWeekday(weekdayName, now) {
345
- const targetDay = WEEKDAY_INDEX.get(weekdayName);
346
- if (targetDay === void 0) {
347
- return null;
348
- }
349
- const today = startOfDayLocal(now);
350
- const currentDay = today.getDay();
351
- const daysBack = (currentDay - targetDay + 7) % 7 || 7;
352
- return addDaysLocal(today, -daysBack);
353
- }
354
- function buildLocalDateAtNoon(year, month, day) {
355
- const parsed = new Date(year, month, day, 12, 0, 0, 0);
356
- if (parsed.getFullYear() !== year || parsed.getMonth() !== month || parsed.getDate() !== day) {
357
- return null;
358
- }
359
- return parsed;
360
- }
361
- function resolveWeekStartDay() {
362
- try {
363
- const locale = Intl.DateTimeFormat().resolvedOptions().locale;
364
- const info = new Intl.Locale(locale).weekInfo;
365
- const firstDay = info?.firstDay;
366
- if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) {
367
- return firstDay % 7;
368
- }
369
- } catch {
370
- }
371
- return 1;
372
- }
373
-
374
- // src/core/episode/scoring.ts
375
- var DAY_IN_MILLISECONDS2 = 24 * 60 * 60 * 1e3;
376
- function scoreEpisodeMatch(episode, bounds, now = /* @__PURE__ */ new Date()) {
377
- const episodeStart = parseEpisodeDate(episode.startedAt);
378
- const episodeEnd = parseEpisodeDate(episode.endedAt ?? episode.startedAt);
379
- const overlapQuality = computeOverlapQuality(episodeStart, episodeEnd, bounds.start, bounds.end);
380
- const midpointProximity = computeMidpointProximity(episodeStart, episodeEnd, bounds.start, bounds.end);
381
- const activity = activityScore(episode.activityLevel);
382
- const recency = recencyScore(episodeEnd, now);
383
- const finalScore = overlapQuality * 0.75 + midpointProximity * 0.2 + activity * 0.04 + recency * 0.01;
384
- return {
385
- result: {
386
- episode,
387
- score: Number(finalScore.toFixed(6)),
388
- scores: {
389
- temporal: Number(overlapQuality.toFixed(6)),
390
- semantic: 0,
391
- activity: Number(activity.toFixed(6)),
392
- recency: Number(recency.toFixed(6))
393
- }
394
- },
395
- explanation: {
396
- overlapQuality: Number(overlapQuality.toFixed(6)),
397
- midpointProximity: Number(midpointProximity.toFixed(6)),
398
- activity: Number(activity.toFixed(6)),
399
- recency: Number(recency.toFixed(6))
400
- }
401
- };
402
- }
403
- function compareEpisodeMatches(left, right) {
404
- return compareDescending(left.explanation.overlapQuality, right.explanation.overlapQuality) || compareDescending(left.explanation.midpointProximity, right.explanation.midpointProximity) || compareDescending(left.explanation.activity, right.explanation.activity) || compareDescending(left.explanation.recency, right.explanation.recency) || compareDescending(left.result.score, right.result.score) || compareAscending(left.result.episode.startedAt, right.result.episode.startedAt) || compareAscending(left.result.episode.id, right.result.episode.id);
405
- }
406
- function computeOverlapQuality(episodeStart, episodeEnd, queryStart, queryEnd) {
407
- const overlapStart = Math.max(episodeStart.getTime(), queryStart.getTime());
408
- const overlapEnd = Math.min(episodeEnd.getTime(), queryEnd.getTime());
409
- const overlapMs = Math.max(0, overlapEnd - overlapStart);
410
- if (overlapMs <= 0) {
411
- return 0;
412
- }
413
- const queryDurationMs = Math.max(1, queryEnd.getTime() - queryStart.getTime());
414
- const episodeDurationMs = Math.max(1, episodeEnd.getTime() - episodeStart.getTime());
415
- const coverage = overlapMs / queryDurationMs;
416
- const precision = overlapMs / episodeDurationMs;
417
- if (coverage <= 0 || precision <= 0) {
418
- return 0;
419
- }
420
- const beta = 0.5;
421
- const betaSquared = beta * beta;
422
- return (1 + betaSquared) * precision * coverage / (betaSquared * precision + coverage);
423
- }
424
- function computeMidpointProximity(episodeStart, episodeEnd, queryStart, queryEnd) {
425
- const episodeMidpoint = (episodeStart.getTime() + episodeEnd.getTime()) / 2;
426
- const queryMidpoint = (queryStart.getTime() + queryEnd.getTime()) / 2;
427
- const queryDurationMs = Math.max(1, queryEnd.getTime() - queryStart.getTime());
428
- const distanceMs = Math.abs(episodeMidpoint - queryMidpoint);
429
- return 1 / (1 + distanceMs / queryDurationMs);
430
- }
431
- function activityScore(value) {
432
- switch (value) {
433
- case "substantial":
434
- return 1;
435
- case "minimal":
436
- return 0.5;
437
- case "none":
438
- return 0;
439
- default:
440
- return 0.25;
441
- }
442
- }
443
- function recencyScore(episodeEnd, now) {
444
- const ageMs = Math.max(0, now.getTime() - episodeEnd.getTime());
445
- const ageDays = ageMs / DAY_IN_MILLISECONDS2;
446
- return 1 / (1 + ageDays / 90);
447
- }
448
- function parseEpisodeDate(value) {
449
- const parsed = new Date(value);
450
- if (Number.isNaN(parsed.getTime())) {
451
- throw new Error(`Episode timestamp is invalid: ${value}`);
452
- }
453
- return parsed;
454
- }
455
- function compareDescending(left, right) {
456
- if (left === right) {
457
- return 0;
458
- }
459
- return right > left ? 1 : -1;
460
- }
461
- function compareAscending(left, right) {
462
- return left.localeCompare(right);
463
- }
464
-
465
- // src/core/episode/search.ts
466
- var DEFAULT_LIMIT = 10;
467
- var MIN_CANDIDATE_LIMIT = 25;
468
- var MAX_CANDIDATE_LIMIT = 100;
469
- var CANDIDATE_MULTIPLIER = 5;
470
- async function searchEpisodes(query, database, now = /* @__PURE__ */ new Date()) {
471
- const limit = normalizeLimit(query.limit);
472
- if (limit === 0) {
473
- return [];
474
- }
475
- const normalizedEmbedding = normalizeEmbedding(query.embedding);
476
- const bounds = query.timeWindow ? resolveTemporalWindowBounds(query.timeWindow, now) : null;
477
- const hasTemporal = bounds !== null;
478
- const hasSemantic = normalizedEmbedding.length > 0;
479
- if (!hasTemporal && !hasSemantic) {
480
- return [];
481
- }
482
- if (hasTemporal && !hasSemantic) {
483
- const candidates2 = await database.listEpisodesByTimeWindow(query.timeWindow, computeCandidateLimit(limit));
484
- return candidates2.map((episode) => scoreEpisodeMatch(episode, bounds, now)).sort(compareEpisodeMatches).slice(0, limit).map((match) => match.result);
485
- }
486
- if (!hasTemporal) {
487
- const matches = await database.episodeVectorSearch({
488
- embedding: normalizedEmbedding,
489
- limit
490
- });
491
- return matches.map((match) => buildSemanticResult(match.episode, match.vectorSim, now)).sort(compareSemanticEpisodeResults).slice(0, limit);
492
- }
493
- const candidates = await database.listEpisodesByTimeWindow(query.timeWindow, computeCandidateLimit(limit));
494
- return candidates.map((episode) => buildHybridResult(episode, normalizedEmbedding, bounds, now)).sort(compareSemanticEpisodeResults).slice(0, limit);
495
- }
496
- function normalizeLimit(value) {
497
- if (value === void 0) {
498
- return DEFAULT_LIMIT;
499
- }
500
- if (!Number.isFinite(value)) {
501
- return DEFAULT_LIMIT;
502
- }
503
- return Math.max(0, Math.trunc(value));
504
- }
505
- function computeCandidateLimit(limit) {
506
- return Math.min(Math.max(limit * CANDIDATE_MULTIPLIER, MIN_CANDIDATE_LIMIT), MAX_CANDIDATE_LIMIT);
507
- }
508
- function normalizeEmbedding(embedding) {
509
- if (!embedding || embedding.length === 0) {
510
- return [];
511
- }
512
- return embedding.map((value) => Number.isFinite(value) ? value : 0);
513
- }
514
- function buildSemanticResult(episode, semantic, now) {
515
- const parsedEpisodeEnd = new Date(episode.endedAt ?? episode.startedAt);
516
- const episodeEnd = Number.isNaN(parsedEpisodeEnd.getTime()) ? now : parsedEpisodeEnd;
517
- const activity = activityScore(episode.activityLevel);
518
- const recency = recencyScore(episodeEnd, now);
519
- const normalizedSemantic = Number(semantic.toFixed(6));
520
- return {
521
- episode,
522
- score: normalizedSemantic,
523
- scores: {
524
- temporal: 0,
525
- semantic: normalizedSemantic,
526
- activity: Number(activity.toFixed(6)),
527
- recency: Number(recency.toFixed(6))
528
- }
529
- };
530
- }
531
- function buildHybridResult(episode, queryEmbedding, bounds, now) {
532
- const temporalMatch = scoreEpisodeMatch(episode, bounds, now);
533
- const semantic = Number(cosineSimilarity(queryEmbedding, episode.embedding ?? []).toFixed(6));
534
- return {
535
- episode,
536
- score: semantic,
537
- scores: {
538
- temporal: temporalMatch.result.scores.temporal,
539
- semantic,
540
- activity: temporalMatch.result.scores.activity,
541
- recency: temporalMatch.result.scores.recency
542
- }
543
- };
544
- }
545
- function compareSemanticEpisodeResults(left, right) {
546
- return compareDescending2(left.scores.semantic, right.scores.semantic) || compareDescending2(left.scores.temporal, right.scores.temporal) || compareDescending2(left.scores.activity, right.scores.activity) || compareDescending2(left.scores.recency, right.scores.recency) || compareDescending2(left.score, right.score) || compareAscending2(left.episode.startedAt, right.episode.startedAt) || compareAscending2(left.episode.id, right.episode.id);
547
- }
548
- function compareDescending2(left, right) {
549
- if (left === right) {
550
- return 0;
551
- }
552
- return right > left ? 1 : -1;
553
- }
554
- function compareAscending2(left, right) {
555
- return left.localeCompare(right);
556
- }
557
-
558
- // src/app/recall/unified.ts
559
- var EPISODE_FRESHNESS_NOTICE = "Episodes cover consolidated prior sessions only; the most recent completed session may not appear yet.";
560
- var EPISODE_SEMANTIC_FALLBACK_NOTICE = "Semantic episode search unavailable - showing temporal results only.";
561
- var EPISODE_SEMANTIC_UNAVAILABLE_NOTICE = "Semantic episode search unavailable - no semantic episode results could be returned.";
562
- var ENTRY_FILTER_NOTICE = "Threshold, type filters, and tag filters were applied to entries only.";
563
- var HISTORICAL_STATE_PATTERNS = [
564
- "what was the previous",
565
- "what was the earlier",
566
- "what did we use before",
567
- "what was the old",
568
- "what changed",
569
- "changed from",
570
- "replaced by",
571
- "before we switched",
572
- "before we migrated",
573
- "previous approach",
574
- "earlier plan",
575
- "old workflow"
576
- ];
577
- var HISTORICAL_STATE_REGEX_PATTERNS = [
578
- /\bwhat\b.*\bused?\b.*\bbefore\b/u,
579
- /\bwhat\b.*\bworkflow\b.*\bbefore\b/u,
580
- /\bwhat\b.*\bplan\b.*\bearlier\b/u,
581
- /\bwhat\b.*\bplan\b.*\bbefore\b/u
582
- ];
583
- async function runUnifiedRecall(input, deps) {
584
- const now = deps.now ?? /* @__PURE__ */ new Date();
585
- const requested = normalizeMode(input.mode);
586
- const parsedTimeWindow = parseTemporalWindow(input.text, now);
587
- const hasEntryFilters = hasEntryScopedFilters(input);
588
- const topicAnchor = hasTopicAnchor(input.text, hasEntryFilters);
589
- const historicalStatePattern = detectHistoricalStatePattern(input.text);
590
- if (historicalStatePattern) {
591
- deps.debugLog?.(`[agenr] unified recall matched historical-state pattern=${JSON.stringify(historicalStatePattern)} query=${JSON.stringify(input.text)}`);
592
- }
593
- const routing = routeRecall({
594
- requested,
595
- text: input.text,
596
- parsedTimeWindow: parsedTimeWindow !== null,
597
- hasEntryFilters
598
- });
599
- const notices = [];
600
- const episodePlan = routing.queried.includes("episodes") ? await buildEpisodeQueryPlan({
601
- text: input.text,
602
- limit: input.limit,
603
- requested,
604
- detectedIntent: routing.detectedIntent,
605
- parsedTimeWindow,
606
- topicAnchor,
607
- embedQuery: deps.embedQuery
608
- }) : {
609
- notices: []
610
- };
611
- const episodes = routing.queried.includes("episodes") && episodePlan.query ? await searchEpisodes(episodePlan.query, deps.database, now) : [];
612
- if (routing.queried.includes("episodes")) {
613
- notices.push(EPISODE_FRESHNESS_NOTICE);
614
- notices.push(...episodePlan.notices);
615
- }
616
- if (routing.queried.includes("episodes") && hasEntryScopedFilters(input)) {
617
- notices.push(ENTRY_FILTER_NOTICE);
618
- }
619
- const entries = await maybeRunEntryRecall({
620
- input,
621
- deps,
622
- parsedTimeWindow,
623
- routing
624
- });
625
- if (routing.queried.includes("entries") && entries.kind === "skipped") {
626
- notices.push(entries.notice);
627
- }
628
- return {
629
- routing,
630
- ...parsedTimeWindow ? {
631
- parsedTimeWindow,
632
- timeWindow: {
633
- start: parsedTimeWindow.bounds.start.toISOString(),
634
- end: parsedTimeWindow.bounds.end.toISOString(),
635
- timezone: parsedTimeWindow.timezone,
636
- resolvedFrom: parsedTimeWindow.resolvedFrom
637
- }
638
- } : {},
639
- episodes,
640
- entries: entries.kind === "results" ? entries.results : [],
641
- notices: dedupePreservingOrder(notices),
642
- count: episodes.length + (entries.kind === "results" ? entries.results.length : 0)
643
- };
644
- }
645
- function routeRecall(params) {
646
- const lower = params.text.trim().toLowerCase();
647
- const factual = /^(when did|when was|what decision|what preference|what(?:'s| is) the default|which version|what threshold)\b/.test(lower);
648
- const narrative = /\b(what happened|what were we doing|what was going on|summarize|catch me up)\b/.test(lower);
649
- const historicalState = detectHistoricalStatePattern(params.text) !== void 0;
650
- const topicAnchor = hasTopicAnchor(params.text, params.hasEntryFilters);
651
- if (params.requested === "entries") {
652
- return {
653
- requested: params.requested,
654
- detectedIntent: historicalState ? "historical_state" : factual ? "factual" : params.parsedTimeWindow ? "mixed" : "factual",
655
- queried: ["entries"],
656
- reason: "Explicit mode=entries override."
657
- };
658
- }
659
- if (params.requested === "episodes") {
660
- return {
661
- requested: params.requested,
662
- detectedIntent: historicalState ? "historical_state" : params.parsedTimeWindow ? "temporal_narrative" : "mixed",
663
- queried: ["episodes"],
664
- reason: params.parsedTimeWindow ? "Explicit mode=episodes override with a resolved time window." : "Explicit mode=episodes override without a resolved time window."
665
- };
666
- }
667
- if (historicalState) {
668
- return {
669
- requested: params.requested,
670
- detectedIntent: "historical_state",
671
- queried: ["entries", "episodes"],
672
- reason: params.parsedTimeWindow ? "The query asks about a previous state or transition and includes a supported time expression, so both entries and episodes were queried." : "The query asks about a previous state or transition, so both entries and episodes were queried."
673
- };
674
- }
675
- if (factual && params.parsedTimeWindow) {
676
- return {
677
- requested: params.requested,
678
- detectedIntent: "mixed",
679
- queried: ["entries", "episodes"],
680
- reason: "The query combines a factual phrase with a supported time expression, so both entries and episodes were queried."
681
- };
682
- }
683
- if (factual) {
684
- return {
685
- requested: params.requested,
686
- detectedIntent: "factual",
687
- queried: ["entries"],
688
- reason: "The query looks like an exact fact lookup, so entry recall was used."
689
- };
690
- }
691
- if (params.parsedTimeWindow && narrative && topicAnchor) {
692
- return {
693
- requested: params.requested,
694
- detectedIntent: "mixed",
695
- queried: ["episodes", "entries"],
696
- reason: "The query combines narrative time-based recall with a topic anchor, so both episodes and entries were queried."
697
- };
698
- }
699
- if (params.parsedTimeWindow && narrative) {
700
- return {
701
- requested: params.requested,
702
- detectedIntent: "temporal_narrative",
703
- queried: ["episodes"],
704
- reason: "The query asks for what happened during a time period, so episode recall was used first."
705
- };
706
- }
707
- if (params.parsedTimeWindow && topicAnchor) {
708
- return {
709
- requested: params.requested,
710
- detectedIntent: "mixed",
711
- queried: ["episodes", "entries"],
712
- reason: "The query contains both a supported time expression and a topic anchor, so both episodes and entries were queried."
713
- };
714
- }
715
- return {
716
- requested: params.requested,
717
- detectedIntent: "factual",
718
- queried: ["entries"],
719
- reason: params.parsedTimeWindow ? "The query did not clearly ask for narrative recall, so entry recall was used." : "No supported episode time window was detected, so entry recall was used."
720
- };
721
- }
722
- async function buildEpisodeQueryPlan(params) {
723
- const notices = [];
724
- const shouldUseSemantic = params.detectedIntent === "historical_state" || (params.parsedTimeWindow ? params.topicAnchor : params.requested === "episodes");
725
- let embedding;
726
- if (shouldUseSemantic) {
727
- embedding = await maybeEmbedEpisodeQuery(params.text, params.embedQuery);
728
- if (!embedding) {
729
- notices.push(params.parsedTimeWindow ? EPISODE_SEMANTIC_FALLBACK_NOTICE : EPISODE_SEMANTIC_UNAVAILABLE_NOTICE);
730
- }
731
- }
732
- if (!params.parsedTimeWindow && !embedding) {
733
- return {
734
- notices
735
- };
736
- }
737
- return {
738
- query: {
739
- text: params.text,
740
- ...params.limit !== void 0 ? { limit: params.limit } : {},
741
- ...params.parsedTimeWindow ? { timeWindow: params.parsedTimeWindow.window } : {},
742
- ...embedding ? { embedding } : {}
743
- },
744
- notices
745
- };
746
- }
747
- async function maybeRunEntryRecall(params) {
748
- if (!params.routing.queried.includes("entries")) {
749
- return {
750
- kind: "results",
751
- results: []
752
- };
753
- }
754
- if (!params.deps.embeddingAvailable) {
755
- const message = params.deps.embeddingError ?? "Embeddings are unavailable, so entry recall could not run.";
756
- if (params.routing.requested === "entries") {
757
- throw new Error(message);
758
- }
759
- return {
760
- kind: "skipped",
761
- notice: `${message} Entry recall was skipped.`
762
- };
763
- }
764
- return {
765
- kind: "results",
766
- results: await recall(buildEntryRecallInput(params.input, params.parsedTimeWindow, params.routing), params.deps.recall, params.deps.recallOptions)
767
- };
768
- }
769
- function buildEntryRecallInput(input, parsedTimeWindow, routing) {
770
- const request = {
771
- text: input.text,
772
- ...input.limit !== void 0 ? { limit: input.limit } : {},
773
- ...input.threshold !== void 0 ? { threshold: input.threshold } : {},
774
- ...input.types && input.types.length > 0 ? { types: input.types } : {},
775
- ...input.tags && input.tags.length > 0 ? { tags: input.tags } : {},
776
- ...input.sessionKey ? { sessionKey: input.sessionKey } : {},
777
- ...routing.detectedIntent === "historical_state" ? { rankingProfile: "historical_state" } : {}
778
- };
779
- if (!parsedTimeWindow) {
780
- return request;
781
- }
782
- const start = parsedTimeWindow.bounds.start;
783
- const end = parsedTimeWindow.bounds.end;
784
- const midpoint = new Date((start.getTime() + end.getTime()) / 2);
785
- const radiusDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 2 / (24 * 60 * 60 * 1e3)));
786
- return {
787
- ...request,
788
- since: start.toISOString(),
789
- until: end.toISOString(),
790
- around: midpoint.toISOString(),
791
- aroundRadius: radiusDays
792
- };
793
- }
794
- function detectHistoricalStatePattern(text) {
795
- const lower = text.trim().toLowerCase();
796
- const explicitPattern = HISTORICAL_STATE_PATTERNS.find((pattern) => lower.includes(pattern));
797
- if (explicitPattern) {
798
- return explicitPattern;
799
- }
800
- const regexPattern = HISTORICAL_STATE_REGEX_PATTERNS.find((pattern) => pattern.test(lower));
801
- return regexPattern?.source;
802
- }
803
- function normalizeMode(value) {
804
- return value === "entries" || value === "episodes" ? value : "auto";
805
- }
806
- function hasEntryScopedFilters(input) {
807
- return Boolean(input.threshold !== void 0 || (input.types?.length ?? 0) > 0 || (input.tags?.length ?? 0) > 0);
808
- }
809
- function hasTopicAnchor(text, hasEntryFilters) {
810
- const lower = text.trim().toLowerCase();
811
- return hasEntryFilters || /\b(about|regarding|with)\b/.test(lower) || /\bon\s+[a-z][a-z0-9_-]{1,}\b/.test(lower);
812
- }
813
- async function maybeEmbedEpisodeQuery(text, embedQuery) {
814
- if (!embedQuery) {
815
- return void 0;
816
- }
817
- try {
818
- const embedding = await embedQuery(text);
819
- return embedding.length > 0 ? embedding : void 0;
820
- } catch {
821
- return void 0;
822
- }
823
- }
824
- function dedupePreservingOrder(values) {
825
- const seen = /* @__PURE__ */ new Set();
826
- const deduped = [];
827
- for (const value of values) {
828
- if (seen.has(value)) {
829
- continue;
830
- }
831
- seen.add(value);
832
- deduped.push(value);
833
- }
834
- return deduped;
835
- }
836
-
837
- export {
838
- runUnifiedRecall
839
- };