@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.
- package/LICENSE +21 -0
- package/README.md +26 -5
- package/dist/bun/agents-file.d.ts +59 -0
- package/dist/bun/agents-file.d.ts.map +1 -0
- package/dist/bun/config.d.ts +58 -0
- package/dist/bun/config.d.ts.map +1 -0
- package/dist/bun/curator.d.ts +35 -0
- package/dist/bun/curator.d.ts.map +1 -0
- package/dist/bun/db/driver.bun.d.ts +5 -0
- package/dist/bun/db/driver.bun.d.ts.map +1 -0
- package/dist/bun/db/driver.node.d.ts +15 -0
- package/dist/bun/db/driver.node.d.ts.map +1 -0
- package/dist/bun/db.d.ts +22 -0
- package/dist/bun/db.d.ts.map +1 -0
- package/dist/bun/distillation.d.ts +32 -0
- package/dist/bun/distillation.d.ts.map +1 -0
- package/dist/bun/embedding.d.ts +90 -0
- package/dist/bun/embedding.d.ts.map +1 -0
- package/dist/bun/gradient.d.ts +73 -0
- package/dist/bun/gradient.d.ts.map +1 -0
- package/dist/bun/index.d.ts +19 -0
- package/dist/bun/index.d.ts.map +1 -0
- package/dist/bun/index.js +28236 -0
- package/dist/bun/index.js.map +7 -0
- package/dist/bun/lat-reader.d.ts +69 -0
- package/dist/bun/lat-reader.d.ts.map +1 -0
- package/dist/bun/log.d.ts +17 -0
- package/dist/bun/log.d.ts.map +1 -0
- package/dist/bun/ltm.d.ts +138 -0
- package/dist/bun/ltm.d.ts.map +1 -0
- package/dist/bun/markdown.d.ts +37 -0
- package/dist/bun/markdown.d.ts.map +1 -0
- package/dist/bun/prompt.d.ts +47 -0
- package/dist/bun/prompt.d.ts.map +1 -0
- package/dist/bun/recall.d.ts +41 -0
- package/dist/bun/recall.d.ts.map +1 -0
- package/dist/bun/search.d.ts +113 -0
- package/dist/bun/search.d.ts.map +1 -0
- package/dist/bun/temporal.d.ts +66 -0
- package/dist/bun/temporal.d.ts.map +1 -0
- package/dist/bun/types.d.ts +180 -0
- package/dist/bun/types.d.ts.map +1 -0
- package/dist/bun/worker.d.ts +6 -0
- package/dist/bun/worker.d.ts.map +1 -0
- package/dist/node/agents-file.d.ts +59 -0
- package/dist/node/agents-file.d.ts.map +1 -0
- package/dist/node/config.d.ts +58 -0
- package/dist/node/config.d.ts.map +1 -0
- package/dist/node/curator.d.ts +35 -0
- package/dist/node/curator.d.ts.map +1 -0
- package/dist/node/db/driver.bun.d.ts +5 -0
- package/dist/node/db/driver.bun.d.ts.map +1 -0
- package/dist/node/db/driver.node.d.ts +15 -0
- package/dist/node/db/driver.node.d.ts.map +1 -0
- package/dist/node/db.d.ts +22 -0
- package/dist/node/db.d.ts.map +1 -0
- package/dist/node/distillation.d.ts +32 -0
- package/dist/node/distillation.d.ts.map +1 -0
- package/dist/node/embedding.d.ts +90 -0
- package/dist/node/embedding.d.ts.map +1 -0
- package/dist/node/gradient.d.ts +73 -0
- package/dist/node/gradient.d.ts.map +1 -0
- package/dist/node/index.d.ts +19 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +28253 -0
- package/dist/node/index.js.map +7 -0
- package/dist/node/lat-reader.d.ts +69 -0
- package/dist/node/lat-reader.d.ts.map +1 -0
- package/dist/node/log.d.ts +17 -0
- package/dist/node/log.d.ts.map +1 -0
- package/dist/node/ltm.d.ts +138 -0
- package/dist/node/ltm.d.ts.map +1 -0
- package/dist/node/markdown.d.ts +37 -0
- package/dist/node/markdown.d.ts.map +1 -0
- package/dist/node/prompt.d.ts +47 -0
- package/dist/node/prompt.d.ts.map +1 -0
- package/dist/node/recall.d.ts +41 -0
- package/dist/node/recall.d.ts.map +1 -0
- package/dist/node/search.d.ts +113 -0
- package/dist/node/search.d.ts.map +1 -0
- package/dist/node/temporal.d.ts +66 -0
- package/dist/node/temporal.d.ts.map +1 -0
- package/dist/node/types.d.ts +180 -0
- package/dist/node/types.d.ts.map +1 -0
- package/dist/node/worker.d.ts +6 -0
- package/dist/node/worker.d.ts.map +1 -0
- package/dist/types/agents-file.d.ts +59 -0
- package/dist/types/agents-file.d.ts.map +1 -0
- package/dist/types/config.d.ts +58 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/curator.d.ts +35 -0
- package/dist/types/curator.d.ts.map +1 -0
- package/dist/types/db/driver.bun.d.ts +5 -0
- package/dist/types/db/driver.bun.d.ts.map +1 -0
- package/dist/types/db/driver.node.d.ts +15 -0
- package/dist/types/db/driver.node.d.ts.map +1 -0
- package/dist/types/db.d.ts +22 -0
- package/dist/types/db.d.ts.map +1 -0
- package/dist/types/distillation.d.ts +32 -0
- package/dist/types/distillation.d.ts.map +1 -0
- package/dist/types/embedding.d.ts +90 -0
- package/dist/types/embedding.d.ts.map +1 -0
- package/dist/types/gradient.d.ts +73 -0
- package/dist/types/gradient.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/lat-reader.d.ts +69 -0
- package/dist/types/lat-reader.d.ts.map +1 -0
- package/dist/types/log.d.ts +17 -0
- package/dist/types/log.d.ts.map +1 -0
- package/dist/types/ltm.d.ts +138 -0
- package/dist/types/ltm.d.ts.map +1 -0
- package/dist/types/markdown.d.ts +37 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/prompt.d.ts +47 -0
- package/dist/types/prompt.d.ts.map +1 -0
- package/dist/types/recall.d.ts +41 -0
- package/dist/types/recall.d.ts.map +1 -0
- package/dist/types/search.d.ts +113 -0
- package/dist/types/search.d.ts.map +1 -0
- package/dist/types/temporal.d.ts +66 -0
- package/dist/types/temporal.d.ts.map +1 -0
- package/dist/types/types.d.ts +180 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/worker.d.ts +6 -0
- package/dist/types/worker.d.ts.map +1 -0
- package/package.json +48 -5
- package/src/agents-file.ts +406 -0
- package/src/config.ts +132 -0
- package/src/curator.ts +220 -0
- package/src/db/driver.bun.ts +18 -0
- package/src/db/driver.node.ts +54 -0
- package/src/db.ts +433 -0
- package/src/distillation.ts +433 -0
- package/src/embedding.ts +528 -0
- package/src/gradient.ts +1387 -0
- package/src/index.ts +109 -0
- package/src/lat-reader.ts +374 -0
- package/src/log.ts +27 -0
- package/src/ltm.ts +861 -0
- package/src/markdown.ts +129 -0
- package/src/prompt.ts +454 -0
- package/src/recall.ts +446 -0
- package/src/search.ts +330 -0
- package/src/temporal.ts +379 -0
- package/src/types.ts +199 -0
- 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
|
+
};
|