@loreai/core 0.0.1 → 0.10.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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -5
  3. package/dist/bun/agents-file.d.ts +59 -0
  4. package/dist/bun/agents-file.d.ts.map +1 -0
  5. package/dist/bun/config.d.ts +58 -0
  6. package/dist/bun/config.d.ts.map +1 -0
  7. package/dist/bun/curator.d.ts +35 -0
  8. package/dist/bun/curator.d.ts.map +1 -0
  9. package/dist/bun/db/driver.bun.d.ts +5 -0
  10. package/dist/bun/db/driver.bun.d.ts.map +1 -0
  11. package/dist/bun/db/driver.node.d.ts +15 -0
  12. package/dist/bun/db/driver.node.d.ts.map +1 -0
  13. package/dist/bun/db.d.ts +22 -0
  14. package/dist/bun/db.d.ts.map +1 -0
  15. package/dist/bun/distillation.d.ts +32 -0
  16. package/dist/bun/distillation.d.ts.map +1 -0
  17. package/dist/bun/embedding.d.ts +90 -0
  18. package/dist/bun/embedding.d.ts.map +1 -0
  19. package/dist/bun/gradient.d.ts +73 -0
  20. package/dist/bun/gradient.d.ts.map +1 -0
  21. package/dist/bun/index.d.ts +19 -0
  22. package/dist/bun/index.d.ts.map +1 -0
  23. package/dist/bun/index.js +28236 -0
  24. package/dist/bun/index.js.map +7 -0
  25. package/dist/bun/lat-reader.d.ts +69 -0
  26. package/dist/bun/lat-reader.d.ts.map +1 -0
  27. package/dist/bun/log.d.ts +17 -0
  28. package/dist/bun/log.d.ts.map +1 -0
  29. package/dist/bun/ltm.d.ts +138 -0
  30. package/dist/bun/ltm.d.ts.map +1 -0
  31. package/dist/bun/markdown.d.ts +37 -0
  32. package/dist/bun/markdown.d.ts.map +1 -0
  33. package/dist/bun/prompt.d.ts +47 -0
  34. package/dist/bun/prompt.d.ts.map +1 -0
  35. package/dist/bun/recall.d.ts +41 -0
  36. package/dist/bun/recall.d.ts.map +1 -0
  37. package/dist/bun/search.d.ts +113 -0
  38. package/dist/bun/search.d.ts.map +1 -0
  39. package/dist/bun/temporal.d.ts +66 -0
  40. package/dist/bun/temporal.d.ts.map +1 -0
  41. package/dist/bun/types.d.ts +180 -0
  42. package/dist/bun/types.d.ts.map +1 -0
  43. package/dist/bun/worker.d.ts +6 -0
  44. package/dist/bun/worker.d.ts.map +1 -0
  45. package/dist/node/agents-file.d.ts +59 -0
  46. package/dist/node/agents-file.d.ts.map +1 -0
  47. package/dist/node/config.d.ts +58 -0
  48. package/dist/node/config.d.ts.map +1 -0
  49. package/dist/node/curator.d.ts +35 -0
  50. package/dist/node/curator.d.ts.map +1 -0
  51. package/dist/node/db/driver.bun.d.ts +5 -0
  52. package/dist/node/db/driver.bun.d.ts.map +1 -0
  53. package/dist/node/db/driver.node.d.ts +15 -0
  54. package/dist/node/db/driver.node.d.ts.map +1 -0
  55. package/dist/node/db.d.ts +22 -0
  56. package/dist/node/db.d.ts.map +1 -0
  57. package/dist/node/distillation.d.ts +32 -0
  58. package/dist/node/distillation.d.ts.map +1 -0
  59. package/dist/node/embedding.d.ts +90 -0
  60. package/dist/node/embedding.d.ts.map +1 -0
  61. package/dist/node/gradient.d.ts +73 -0
  62. package/dist/node/gradient.d.ts.map +1 -0
  63. package/dist/node/index.d.ts +19 -0
  64. package/dist/node/index.d.ts.map +1 -0
  65. package/dist/node/index.js +28253 -0
  66. package/dist/node/index.js.map +7 -0
  67. package/dist/node/lat-reader.d.ts +69 -0
  68. package/dist/node/lat-reader.d.ts.map +1 -0
  69. package/dist/node/log.d.ts +17 -0
  70. package/dist/node/log.d.ts.map +1 -0
  71. package/dist/node/ltm.d.ts +138 -0
  72. package/dist/node/ltm.d.ts.map +1 -0
  73. package/dist/node/markdown.d.ts +37 -0
  74. package/dist/node/markdown.d.ts.map +1 -0
  75. package/dist/node/prompt.d.ts +47 -0
  76. package/dist/node/prompt.d.ts.map +1 -0
  77. package/dist/node/recall.d.ts +41 -0
  78. package/dist/node/recall.d.ts.map +1 -0
  79. package/dist/node/search.d.ts +113 -0
  80. package/dist/node/search.d.ts.map +1 -0
  81. package/dist/node/temporal.d.ts +66 -0
  82. package/dist/node/temporal.d.ts.map +1 -0
  83. package/dist/node/types.d.ts +180 -0
  84. package/dist/node/types.d.ts.map +1 -0
  85. package/dist/node/worker.d.ts +6 -0
  86. package/dist/node/worker.d.ts.map +1 -0
  87. package/dist/types/agents-file.d.ts +59 -0
  88. package/dist/types/agents-file.d.ts.map +1 -0
  89. package/dist/types/config.d.ts +58 -0
  90. package/dist/types/config.d.ts.map +1 -0
  91. package/dist/types/curator.d.ts +35 -0
  92. package/dist/types/curator.d.ts.map +1 -0
  93. package/dist/types/db/driver.bun.d.ts +5 -0
  94. package/dist/types/db/driver.bun.d.ts.map +1 -0
  95. package/dist/types/db/driver.node.d.ts +15 -0
  96. package/dist/types/db/driver.node.d.ts.map +1 -0
  97. package/dist/types/db.d.ts +22 -0
  98. package/dist/types/db.d.ts.map +1 -0
  99. package/dist/types/distillation.d.ts +32 -0
  100. package/dist/types/distillation.d.ts.map +1 -0
  101. package/dist/types/embedding.d.ts +90 -0
  102. package/dist/types/embedding.d.ts.map +1 -0
  103. package/dist/types/gradient.d.ts +73 -0
  104. package/dist/types/gradient.d.ts.map +1 -0
  105. package/dist/types/index.d.ts +19 -0
  106. package/dist/types/index.d.ts.map +1 -0
  107. package/dist/types/lat-reader.d.ts +69 -0
  108. package/dist/types/lat-reader.d.ts.map +1 -0
  109. package/dist/types/log.d.ts +17 -0
  110. package/dist/types/log.d.ts.map +1 -0
  111. package/dist/types/ltm.d.ts +138 -0
  112. package/dist/types/ltm.d.ts.map +1 -0
  113. package/dist/types/markdown.d.ts +37 -0
  114. package/dist/types/markdown.d.ts.map +1 -0
  115. package/dist/types/prompt.d.ts +47 -0
  116. package/dist/types/prompt.d.ts.map +1 -0
  117. package/dist/types/recall.d.ts +41 -0
  118. package/dist/types/recall.d.ts.map +1 -0
  119. package/dist/types/search.d.ts +113 -0
  120. package/dist/types/search.d.ts.map +1 -0
  121. package/dist/types/temporal.d.ts +66 -0
  122. package/dist/types/temporal.d.ts.map +1 -0
  123. package/dist/types/types.d.ts +180 -0
  124. package/dist/types/types.d.ts.map +1 -0
  125. package/dist/types/worker.d.ts +6 -0
  126. package/dist/types/worker.d.ts.map +1 -0
  127. package/package.json +48 -5
  128. package/src/agents-file.ts +406 -0
  129. package/src/config.ts +132 -0
  130. package/src/curator.ts +220 -0
  131. package/src/db/driver.bun.ts +18 -0
  132. package/src/db/driver.node.ts +54 -0
  133. package/src/db.ts +433 -0
  134. package/src/distillation.ts +433 -0
  135. package/src/embedding.ts +528 -0
  136. package/src/gradient.ts +1387 -0
  137. package/src/index.ts +109 -0
  138. package/src/lat-reader.ts +374 -0
  139. package/src/log.ts +27 -0
  140. package/src/ltm.ts +861 -0
  141. package/src/markdown.ts +129 -0
  142. package/src/prompt.ts +454 -0
  143. package/src/recall.ts +446 -0
  144. package/src/search.ts +330 -0
  145. package/src/temporal.ts +379 -0
  146. package/src/types.ts +199 -0
  147. package/src/worker.ts +26 -0
package/src/recall.ts ADDED
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Recall — unified search across Lore's memory sources.
3
+ *
4
+ * Pure search + result-formatting logic shared by every host's recall tool.
5
+ * Hosts (OpenCode plugin, Pi extension, future ACP server) wrap `runRecall()`
6
+ * in their tool-registration mechanism:
7
+ * - OpenCode: `tool()` from `@opencode-ai/plugin/tool`
8
+ * - Pi: `pi.registerTool()` with TypeBox schema
9
+ *
10
+ * Behavior is identical across hosts so curated knowledge travels with the user.
11
+ */
12
+ import * as latReader from "./lat-reader";
13
+ import * as ltm from "./ltm";
14
+ import * as temporal from "./temporal";
15
+ import * as embedding from "./embedding";
16
+ import * as log from "./log";
17
+ import { db, ensureProject, projectName } from "./db";
18
+ import type { LoreConfig } from "./config";
19
+ import type { LLMClient } from "./types";
20
+ import {
21
+ EMPTY_QUERY,
22
+ expandQuery,
23
+ ftsQuery,
24
+ ftsQueryOr,
25
+ reciprocalRankFusion,
26
+ } from "./search";
27
+ import { h, inline, lip, liph, p, root, serialize, t, ul } from "./markdown";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Types
31
+ // ---------------------------------------------------------------------------
32
+
33
+ type Distillation = {
34
+ id: string;
35
+ observations: string;
36
+ generation: number;
37
+ created_at: number;
38
+ session_id: string;
39
+ };
40
+
41
+ export type ScoredDistillation = Distillation & { rank: number };
42
+
43
+ export type RecallScope = "all" | "session" | "project" | "knowledge";
44
+
45
+ export type RecallInput = {
46
+ query: string;
47
+ /** Narrow the search surface. Defaults to `"all"`. */
48
+ scope?: RecallScope;
49
+ /** Project root — used by all scoring paths. */
50
+ projectPath: string;
51
+ /** Current session ID — required when `scope === "session"`. */
52
+ sessionID?: string;
53
+ /** Whether to include long-term knowledge results. Default `true`. */
54
+ knowledgeEnabled?: boolean;
55
+ /** Optional LLM client for query expansion (if `config.search.queryExpansion`). */
56
+ llm?: LLMClient;
57
+ /** Search config — provides recallLimit, queryExpansion, ftsWeights, etc. */
58
+ searchConfig?: LoreConfig["search"];
59
+ };
60
+
61
+ /** Result of a full recall run — markdown-formatted string for the LLM. */
62
+ export type RecallResult = string;
63
+
64
+ type TaggedResult =
65
+ | { source: "knowledge"; item: ltm.ScoredKnowledgeEntry }
66
+ | {
67
+ source: "cross-knowledge";
68
+ item: ltm.ScoredKnowledgeEntry;
69
+ projectLabel: string;
70
+ }
71
+ | { source: "distillation"; item: ScoredDistillation }
72
+ | { source: "temporal"; item: temporal.ScoredTemporalMessage }
73
+ | { source: "lat-section"; item: latReader.ScoredLatSection };
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Distillation search
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /** LIKE-based fallback for when FTS5 fails unexpectedly on distillations. */
80
+ function searchDistillationsLike(input: {
81
+ pid: string;
82
+ query: string;
83
+ sessionID?: string;
84
+ limit: number;
85
+ }): Distillation[] {
86
+ const terms = input.query
87
+ .toLowerCase()
88
+ .split(/\s+/)
89
+ .filter((term) => term.length > 1);
90
+ if (!terms.length) return [];
91
+ const conditions = terms
92
+ .map(() => "LOWER(observations) LIKE ?")
93
+ .join(" AND ");
94
+ const likeParams = terms.map((term) => `%${term}%`);
95
+ const sql = input.sessionID
96
+ ? `SELECT id, observations, generation, created_at, session_id FROM distillations WHERE project_id = ? AND session_id = ? AND ${conditions} ORDER BY created_at DESC LIMIT ?`
97
+ : `SELECT id, observations, generation, created_at, session_id FROM distillations WHERE project_id = ? AND ${conditions} ORDER BY created_at DESC LIMIT ?`;
98
+ const allParams = input.sessionID
99
+ ? [input.pid, input.sessionID, ...likeParams, input.limit]
100
+ : [input.pid, ...likeParams, input.limit];
101
+ return db()
102
+ .query(sql)
103
+ .all(...allParams) as Distillation[];
104
+ }
105
+
106
+ function searchDistillationsScored(input: {
107
+ projectPath: string;
108
+ query: string;
109
+ sessionID?: string;
110
+ limit?: number;
111
+ }): ScoredDistillation[] {
112
+ const pid = ensureProject(input.projectPath);
113
+ const limit = input.limit ?? 10;
114
+ const q = ftsQuery(input.query);
115
+ if (q === EMPTY_QUERY) return [];
116
+
117
+ const ftsSQL = input.sessionID
118
+ ? `SELECT d.id, d.observations, d.generation, d.created_at, d.session_id, rank
119
+ FROM distillations d
120
+ JOIN distillation_fts f ON d.rowid = f.rowid
121
+ WHERE distillation_fts MATCH ?
122
+ AND d.project_id = ? AND d.session_id = ?
123
+ ORDER BY rank LIMIT ?`
124
+ : `SELECT d.id, d.observations, d.generation, d.created_at, d.session_id, rank
125
+ FROM distillations d
126
+ JOIN distillation_fts f ON d.rowid = f.rowid
127
+ WHERE distillation_fts MATCH ?
128
+ AND d.project_id = ?
129
+ ORDER BY rank LIMIT ?`;
130
+ const params = input.sessionID
131
+ ? [q, pid, input.sessionID, limit]
132
+ : [q, pid, limit];
133
+
134
+ try {
135
+ const results = db().query(ftsSQL).all(...params) as ScoredDistillation[];
136
+ if (results.length) return results;
137
+
138
+ // AND returned nothing — try OR fallback
139
+ const qOr = ftsQueryOr(input.query);
140
+ if (qOr === EMPTY_QUERY) return [];
141
+ const paramsOr = input.sessionID
142
+ ? [qOr, pid, input.sessionID, limit]
143
+ : [qOr, pid, limit];
144
+ return db().query(ftsSQL).all(...paramsOr) as ScoredDistillation[];
145
+ } catch {
146
+ // FTS5 failed — fall back to LIKE search with synthetic rank
147
+ return searchDistillationsLike({
148
+ pid,
149
+ query: input.query,
150
+ sessionID: input.sessionID,
151
+ limit,
152
+ }).map((dist, i) => ({ ...dist, rank: -(10 - i) }));
153
+ }
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Result formatting
158
+ // ---------------------------------------------------------------------------
159
+
160
+ function formatFusedResults(
161
+ results: Array<{ item: TaggedResult; score: number }>,
162
+ maxResults: number,
163
+ ): string {
164
+ if (!results.length) return "No results found for this query.";
165
+
166
+ const items = results.slice(0, maxResults).map(({ item: tagged }) => {
167
+ switch (tagged.source) {
168
+ case "knowledge": {
169
+ const k = tagged.item;
170
+ return liph(
171
+ t(
172
+ `**[knowledge/${k.category}]** ${inline(k.title)}: ${inline(k.content)}`,
173
+ ),
174
+ );
175
+ }
176
+ case "cross-knowledge": {
177
+ const k = tagged.item;
178
+ return liph(
179
+ t(
180
+ `**[knowledge/${k.category} from: ${tagged.projectLabel}]** ${inline(k.title)}: ${inline(k.content)}`,
181
+ ),
182
+ );
183
+ }
184
+ case "distillation": {
185
+ const d = tagged.item;
186
+ const preview =
187
+ d.observations.length > 500
188
+ ? d.observations.slice(0, 500) + "..."
189
+ : d.observations;
190
+ return lip(`**[distilled]** ${inline(preview)}`);
191
+ }
192
+ case "temporal": {
193
+ const m = tagged.item;
194
+ const preview =
195
+ m.content.length > 500 ? m.content.slice(0, 500) + "..." : m.content;
196
+ return lip(
197
+ `**[temporal/${m.role}]** (session: ${m.session_id.slice(0, 8)}...) ${inline(preview)}`,
198
+ );
199
+ }
200
+ case "lat-section": {
201
+ const s = tagged.item;
202
+ const preview = s.first_paragraph
203
+ ? inline(s.first_paragraph)
204
+ : inline(
205
+ s.content.length > 300 ? s.content.slice(0, 300) + "..." : s.content,
206
+ );
207
+ return liph(
208
+ t(`**[lat.md/${s.file}]** ${inline(s.heading)}: ${preview}`),
209
+ );
210
+ }
211
+ }
212
+ });
213
+
214
+ return serialize(root(h(2, "Recall Results"), ul(items)));
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Main entry point
219
+ // ---------------------------------------------------------------------------
220
+
221
+ /** Full recall run: search every relevant source, fuse with RRF, format as markdown. */
222
+ export async function runRecall(input: RecallInput): Promise<RecallResult> {
223
+ const {
224
+ query,
225
+ scope = "all",
226
+ projectPath,
227
+ sessionID,
228
+ knowledgeEnabled = true,
229
+ llm,
230
+ searchConfig,
231
+ } = input;
232
+
233
+ const limit = searchConfig?.recallLimit ?? 10;
234
+
235
+ // Short-circuit vague queries — stopwords-only would match everything.
236
+ if (ftsQuery(query) === EMPTY_QUERY) {
237
+ return "Query too vague — try using specific keywords, file names, or technical terms.";
238
+ }
239
+
240
+ // Optional query expansion: generate alternative phrasings via LLM.
241
+ let queries = [query];
242
+ if (searchConfig?.queryExpansion && llm) {
243
+ try {
244
+ queries = await expandQuery(llm, query);
245
+ } catch (err) {
246
+ log.info("recall: query expansion failed, using original:", err);
247
+ }
248
+ }
249
+
250
+ // Collect per-query RRF lists. Original query is always first; if expansion
251
+ // produced extras, we still weight the original twice by adding both original
252
+ // and expanded lists (RRF naturally weights items appearing in more lists).
253
+ const allRrfLists: Array<{
254
+ items: TaggedResult[];
255
+ key: (r: TaggedResult) => string;
256
+ }> = [];
257
+
258
+ for (const q of queries) {
259
+ const knowledgeResults: ltm.ScoredKnowledgeEntry[] = [];
260
+ if (knowledgeEnabled && scope !== "session") {
261
+ try {
262
+ knowledgeResults.push(
263
+ ...ltm.searchScored({ query: q, projectPath, limit }),
264
+ );
265
+ } catch (err) {
266
+ log.error("recall: knowledge search failed:", err);
267
+ }
268
+ }
269
+
270
+ const distillationResults: ScoredDistillation[] = [];
271
+ if (scope !== "knowledge") {
272
+ try {
273
+ distillationResults.push(
274
+ ...searchDistillationsScored({
275
+ projectPath,
276
+ query: q,
277
+ sessionID: scope === "session" ? sessionID : undefined,
278
+ limit,
279
+ }),
280
+ );
281
+ } catch (err) {
282
+ log.error("recall: distillation search failed:", err);
283
+ }
284
+ }
285
+
286
+ const temporalResults: temporal.ScoredTemporalMessage[] = [];
287
+ if (scope !== "knowledge") {
288
+ try {
289
+ temporalResults.push(
290
+ ...temporal.searchScored({
291
+ projectPath,
292
+ query: q,
293
+ sessionID: scope === "session" ? sessionID : undefined,
294
+ limit,
295
+ }),
296
+ );
297
+ } catch (err) {
298
+ log.error("recall: temporal search failed:", err);
299
+ }
300
+ }
301
+
302
+ allRrfLists.push(
303
+ {
304
+ items: knowledgeResults.map((item) => ({
305
+ source: "knowledge" as const,
306
+ item,
307
+ })),
308
+ key: (r) => `k:${r.item.id}`,
309
+ },
310
+ {
311
+ items: distillationResults.map((item) => ({
312
+ source: "distillation" as const,
313
+ item,
314
+ })),
315
+ key: (r) => `d:${r.item.id}`,
316
+ },
317
+ {
318
+ items: temporalResults.map((item) => ({
319
+ source: "temporal" as const,
320
+ item,
321
+ })),
322
+ key: (r) => `t:${r.item.id}`,
323
+ },
324
+ );
325
+ }
326
+
327
+ // Vector search on the original query (not expansions — avoid redundant embeds).
328
+ if (embedding.isAvailable() && scope !== "session") {
329
+ try {
330
+ const [queryVec] = await embedding.embed([query], "query");
331
+
332
+ // Knowledge vector search
333
+ if (knowledgeEnabled) {
334
+ const vectorHits = embedding.vectorSearch(queryVec, limit);
335
+ const vectorTagged: TaggedResult[] = [];
336
+ for (const hit of vectorHits) {
337
+ const entry = ltm.get(hit.id);
338
+ if (entry) {
339
+ vectorTagged.push({
340
+ source: "knowledge",
341
+ item: { ...entry, rank: -hit.similarity },
342
+ });
343
+ }
344
+ }
345
+ if (vectorTagged.length) {
346
+ // Same `k:` key prefix as BM25 knowledge — RRF merges, not duplicates
347
+ allRrfLists.push({
348
+ items: vectorTagged,
349
+ key: (r) => `k:${r.item.id}`,
350
+ });
351
+ }
352
+ }
353
+
354
+ // Distillation vector search
355
+ if (scope !== "knowledge") {
356
+ const distVectorHits = embedding.vectorSearchDistillations(queryVec, limit);
357
+ const distVectorTagged: TaggedResult[] = distVectorHits
358
+ .map((hit): TaggedResult | null => {
359
+ const row = db()
360
+ .query(
361
+ "SELECT id, observations, generation, created_at, session_id FROM distillations WHERE id = ?",
362
+ )
363
+ .get(hit.id) as Distillation | null;
364
+ if (!row) return null;
365
+ return {
366
+ source: "distillation",
367
+ item: { ...row, rank: -hit.similarity },
368
+ };
369
+ })
370
+ .filter((r): r is TaggedResult => r !== null);
371
+ if (distVectorTagged.length) {
372
+ allRrfLists.push({
373
+ items: distVectorTagged,
374
+ key: (r) => `d:${r.item.id}`,
375
+ });
376
+ }
377
+ }
378
+ } catch (err) {
379
+ log.info("recall: vector search failed:", err);
380
+ }
381
+ }
382
+
383
+ // lat.md section search
384
+ if (scope !== "session" && latReader.hasLatDir(projectPath)) {
385
+ try {
386
+ const latResults = latReader.searchScored({
387
+ query,
388
+ projectPath,
389
+ limit,
390
+ });
391
+ if (latResults.length) {
392
+ allRrfLists.push({
393
+ items: latResults.map((item) => ({
394
+ source: "lat-section" as const,
395
+ item,
396
+ })),
397
+ key: (r) =>
398
+ `lat:${(r as { source: "lat-section"; item: latReader.ScoredLatSection }).item.id}`,
399
+ });
400
+ }
401
+ } catch (err) {
402
+ log.info("recall: lat.md section search failed:", err);
403
+ }
404
+ }
405
+
406
+ // Cross-project knowledge discovery — only in "all" scope.
407
+ if (knowledgeEnabled && scope === "all") {
408
+ try {
409
+ const crossProjectResults = ltm.searchScoredOtherProjects({
410
+ query,
411
+ excludeProjectPath: projectPath,
412
+ limit,
413
+ });
414
+ if (crossProjectResults.length) {
415
+ allRrfLists.push({
416
+ items: crossProjectResults.map((item: ltm.ScoredKnowledgeEntry) => {
417
+ const label =
418
+ (item.project_id ? projectName(item.project_id) : null) ?? "other";
419
+ return {
420
+ source: "cross-knowledge" as const,
421
+ item,
422
+ projectLabel: label,
423
+ } as TaggedResult;
424
+ }),
425
+ key: (r) => `xk:${r.item.id}`,
426
+ });
427
+ }
428
+ } catch (err) {
429
+ log.info("recall: cross-project knowledge search failed:", err);
430
+ }
431
+ }
432
+
433
+ const fused = reciprocalRankFusion<TaggedResult>(allRrfLists);
434
+ return formatFusedResults(fused, 20);
435
+ }
436
+
437
+ /** Standard tool description reused verbatim by each host adapter. */
438
+ export const RECALL_TOOL_DESCRIPTION =
439
+ "Search your persistent memory for this project. Your visible context is a trimmed window — older messages, decisions, and details may not be visible to you even within the current session. Use this tool whenever you need information that isn't in your current context: file paths, past decisions, user preferences, prior approaches, or anything from earlier in this conversation or previous sessions. Always prefer recall over assuming you don't have the information. Searches long-term knowledge, distilled history, and raw message archives.";
440
+
441
+ /** Standard parameter descriptions reused by each host adapter. */
442
+ export const RECALL_PARAM_DESCRIPTIONS = {
443
+ query: "What to search for — be specific. Include keywords, file names, or concepts.",
444
+ scope:
445
+ "Search scope: 'all' (default) searches everything, 'session' searches current session only, 'project' searches all sessions in this project, 'knowledge' searches only long-term knowledge.",
446
+ };