@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/config.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const LoreConfig = z.object({
|
|
6
|
+
model: z
|
|
7
|
+
.object({
|
|
8
|
+
providerID: z.string(),
|
|
9
|
+
modelID: z.string(),
|
|
10
|
+
})
|
|
11
|
+
.optional(),
|
|
12
|
+
budget: z
|
|
13
|
+
.object({
|
|
14
|
+
distilled: z.number().min(0.05).max(0.5).default(0.25),
|
|
15
|
+
raw: z.number().min(0.1).max(0.7).default(0.4),
|
|
16
|
+
output: z.number().min(0.1).max(0.5).default(0.25),
|
|
17
|
+
/** Max fraction of usable context reserved for LTM system-prompt injection. Default: 0.10 (10%). */
|
|
18
|
+
ltm: z.number().min(0.02).max(0.3).default(0.10),
|
|
19
|
+
})
|
|
20
|
+
.default({ distilled: 0.25, raw: 0.4, output: 0.25, ltm: 0.10 }),
|
|
21
|
+
distillation: z
|
|
22
|
+
.object({
|
|
23
|
+
minMessages: z.number().min(3).default(8),
|
|
24
|
+
maxSegment: z.number().min(5).default(50),
|
|
25
|
+
metaThreshold: z.number().min(3).default(10),
|
|
26
|
+
})
|
|
27
|
+
.default({ minMessages: 8, maxSegment: 50, metaThreshold: 10 }),
|
|
28
|
+
knowledge: z
|
|
29
|
+
.object({
|
|
30
|
+
/** Set to false to disable long-term knowledge storage and system-prompt injection.
|
|
31
|
+
* Conversation recall (temporal search, distillation search) and context management
|
|
32
|
+
* (gradient transform, distillation) remain fully active. Disabling this turns off
|
|
33
|
+
* the curator, knowledge DB writes, AGENTS.md sync, and LTM injection into the
|
|
34
|
+
* system prompt. Default: true. */
|
|
35
|
+
enabled: z.boolean().default(true),
|
|
36
|
+
})
|
|
37
|
+
.default({ enabled: true }),
|
|
38
|
+
curator: z
|
|
39
|
+
.object({
|
|
40
|
+
enabled: z.boolean().default(true),
|
|
41
|
+
onIdle: z.boolean().default(true),
|
|
42
|
+
afterTurns: z.number().min(1).default(3),
|
|
43
|
+
/** Max knowledge entries per project before consolidation triggers. Default: 25. */
|
|
44
|
+
maxEntries: z.number().min(10).default(25),
|
|
45
|
+
})
|
|
46
|
+
.default({ enabled: true, onIdle: true, afterTurns: 3, maxEntries: 25 }),
|
|
47
|
+
pruning: z
|
|
48
|
+
.object({
|
|
49
|
+
/** Days to keep distilled temporal messages before pruning. Default: 120. */
|
|
50
|
+
retention: z.number().min(1).default(120),
|
|
51
|
+
/** Max total temporal_messages storage in MB before emergency pruning. Default: 1024 (1 GB). */
|
|
52
|
+
maxStorage: z.number().min(50).default(1024),
|
|
53
|
+
})
|
|
54
|
+
.default({ retention: 120, maxStorage: 1024 }),
|
|
55
|
+
search: z
|
|
56
|
+
.object({
|
|
57
|
+
/** BM25 column weights for knowledge FTS5 [title, content, category]. */
|
|
58
|
+
ftsWeights: z
|
|
59
|
+
.object({
|
|
60
|
+
title: z.number().min(0).default(6.0),
|
|
61
|
+
content: z.number().min(0).default(2.0),
|
|
62
|
+
category: z.number().min(0).default(3.0),
|
|
63
|
+
})
|
|
64
|
+
.default({ title: 6.0, content: 2.0, category: 3.0 }),
|
|
65
|
+
/** Max results per source in recall tool before fusion. Default: 10. */
|
|
66
|
+
recallLimit: z.number().min(1).max(50).default(10),
|
|
67
|
+
/** Enable LLM-based query expansion for the recall tool. Default: false.
|
|
68
|
+
* When enabled, the configured model generates 2–3 alternative query phrasings
|
|
69
|
+
* before search, improving recall for ambiguous queries. */
|
|
70
|
+
queryExpansion: z.boolean().default(false),
|
|
71
|
+
/** Vector embedding search.
|
|
72
|
+
* Supports multiple providers: "voyage" (Voyage AI, VOYAGE_API_KEY),
|
|
73
|
+
* "openai" (OpenAI, OPENAI_API_KEY).
|
|
74
|
+
* Automatically enabled when the configured provider's API key env var is set.
|
|
75
|
+
* Set enabled: false to explicitly disable even with the key present. */
|
|
76
|
+
embeddings: z
|
|
77
|
+
.object({
|
|
78
|
+
/** Enable/disable vector embedding search. Default: true.
|
|
79
|
+
* Set to false to explicitly disable even when the API key is set. */
|
|
80
|
+
enabled: z.boolean().default(true),
|
|
81
|
+
/** Embedding provider. Default: "voyage".
|
|
82
|
+
* Each provider reads its own env var for the API key:
|
|
83
|
+
* - "voyage": VOYAGE_API_KEY (default model: voyage-code-3, 1024 dims)
|
|
84
|
+
* - "openai": OPENAI_API_KEY (default model: text-embedding-3-small, 1536 dims) */
|
|
85
|
+
provider: z.enum(["voyage", "openai"]).default("voyage"),
|
|
86
|
+
/** Model ID for the embedding provider. Default depends on provider. */
|
|
87
|
+
model: z.string().default("voyage-code-3"),
|
|
88
|
+
/** Embedding dimensions. Default: 1024. */
|
|
89
|
+
dimensions: z.number().min(256).max(2048).default(1024),
|
|
90
|
+
})
|
|
91
|
+
.default({
|
|
92
|
+
enabled: true,
|
|
93
|
+
provider: "voyage",
|
|
94
|
+
model: "voyage-code-3",
|
|
95
|
+
dimensions: 1024,
|
|
96
|
+
}),
|
|
97
|
+
})
|
|
98
|
+
.default({
|
|
99
|
+
ftsWeights: { title: 6.0, content: 2.0, category: 3.0 },
|
|
100
|
+
recallLimit: 10,
|
|
101
|
+
queryExpansion: false,
|
|
102
|
+
embeddings: { enabled: true, provider: "voyage" as const, model: "voyage-code-3", dimensions: 1024 },
|
|
103
|
+
}),
|
|
104
|
+
crossProject: z.boolean().default(false),
|
|
105
|
+
agentsFile: z
|
|
106
|
+
.object({
|
|
107
|
+
/** Set to false to disable all AGENTS.md export/import behaviour. */
|
|
108
|
+
enabled: z.boolean().default(true),
|
|
109
|
+
/** Path to the agents file, relative to the project root. */
|
|
110
|
+
path: z.string().default("AGENTS.md"),
|
|
111
|
+
})
|
|
112
|
+
.default({ enabled: true, path: "AGENTS.md" }),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export type LoreConfig = z.infer<typeof LoreConfig>;
|
|
116
|
+
|
|
117
|
+
let current: LoreConfig = LoreConfig.parse({});
|
|
118
|
+
|
|
119
|
+
export function config(): LoreConfig {
|
|
120
|
+
return current;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function load(directory: string): Promise<LoreConfig> {
|
|
124
|
+
const path = join(directory, ".lore.json");
|
|
125
|
+
if (existsSync(path)) {
|
|
126
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
127
|
+
current = LoreConfig.parse(raw);
|
|
128
|
+
return current;
|
|
129
|
+
}
|
|
130
|
+
current = LoreConfig.parse({});
|
|
131
|
+
return current;
|
|
132
|
+
}
|
package/src/curator.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { config } from "./config";
|
|
2
|
+
import * as temporal from "./temporal";
|
|
3
|
+
import * as ltm from "./ltm";
|
|
4
|
+
import * as log from "./log";
|
|
5
|
+
import { CURATOR_SYSTEM, curatorUser, CONSOLIDATION_SYSTEM, consolidationUser } from "./prompt";
|
|
6
|
+
import type { LLMClient } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Maximum length (chars) for a single knowledge entry's content.
|
|
10
|
+
* ~400 tokens at chars/3. Entries exceeding this are truncated with a notice.
|
|
11
|
+
* The curator prompt also instructs the model to stay within this limit,
|
|
12
|
+
* so truncation is a last-resort safety net.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_ENTRY_CONTENT_LENGTH = 1200;
|
|
15
|
+
|
|
16
|
+
type CuratorOp =
|
|
17
|
+
| {
|
|
18
|
+
op: "create";
|
|
19
|
+
category: string;
|
|
20
|
+
title: string;
|
|
21
|
+
content: string;
|
|
22
|
+
scope: "project" | "global";
|
|
23
|
+
crossProject?: boolean;
|
|
24
|
+
}
|
|
25
|
+
| { op: "update"; id: string; content?: string; confidence?: number }
|
|
26
|
+
| { op: "delete"; id: string; reason: string };
|
|
27
|
+
|
|
28
|
+
function parseOps(text: string): CuratorOp[] {
|
|
29
|
+
const cleaned = text
|
|
30
|
+
.trim()
|
|
31
|
+
.replace(/^```json?\s*/i, "")
|
|
32
|
+
.replace(/\s*```$/i, "");
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(cleaned);
|
|
35
|
+
if (!Array.isArray(parsed)) return [];
|
|
36
|
+
return parsed.filter(
|
|
37
|
+
(op: unknown) =>
|
|
38
|
+
typeof op === "object" &&
|
|
39
|
+
op !== null &&
|
|
40
|
+
"op" in op &&
|
|
41
|
+
typeof (op as Record<string, unknown>).op === "string",
|
|
42
|
+
) as CuratorOp[];
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Track which messages we've already curated — per session to prevent
|
|
49
|
+
// cross-session leaking (curation on session A advancing the timestamp
|
|
50
|
+
// past session B's messages, causing B's curation to find < 3 recent).
|
|
51
|
+
const lastCuratedAt = new Map<string, number>();
|
|
52
|
+
|
|
53
|
+
export async function run(input: {
|
|
54
|
+
llm: LLMClient;
|
|
55
|
+
projectPath: string;
|
|
56
|
+
sessionID: string;
|
|
57
|
+
model?: { providerID: string; modelID: string };
|
|
58
|
+
}): Promise<{ created: number; updated: number; deleted: number }> {
|
|
59
|
+
const cfg = config();
|
|
60
|
+
if (!cfg.curator.enabled) return { created: 0, updated: 0, deleted: 0 };
|
|
61
|
+
|
|
62
|
+
// Get recent messages since last curation
|
|
63
|
+
const all = temporal.bySession(input.projectPath, input.sessionID);
|
|
64
|
+
const sessionCuratedAt = lastCuratedAt.get(input.sessionID) ?? 0;
|
|
65
|
+
const recent = all.filter((m) => m.created_at > sessionCuratedAt);
|
|
66
|
+
if (recent.length < 3) return { created: 0, updated: 0, deleted: 0 };
|
|
67
|
+
|
|
68
|
+
const text = recent.map((m) => `[${m.role}] ${m.content}`).join("\n\n");
|
|
69
|
+
const existing = ltm.forProject(input.projectPath, false);
|
|
70
|
+
const existingForPrompt = existing.map((e) => ({
|
|
71
|
+
id: e.id,
|
|
72
|
+
category: e.category,
|
|
73
|
+
title: e.title,
|
|
74
|
+
content: e.content,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const userContent = curatorUser({
|
|
78
|
+
messages: text,
|
|
79
|
+
existing: existingForPrompt,
|
|
80
|
+
});
|
|
81
|
+
const model = input.model ?? cfg.model;
|
|
82
|
+
const responseText = await input.llm.prompt(
|
|
83
|
+
CURATOR_SYSTEM,
|
|
84
|
+
userContent,
|
|
85
|
+
{ model, workerID: "lore-curator" },
|
|
86
|
+
);
|
|
87
|
+
if (!responseText) return { created: 0, updated: 0, deleted: 0 };
|
|
88
|
+
|
|
89
|
+
const ops = parseOps(responseText);
|
|
90
|
+
let created = 0;
|
|
91
|
+
let updated = 0;
|
|
92
|
+
let deleted = 0;
|
|
93
|
+
|
|
94
|
+
const idsToSync: string[] = [];
|
|
95
|
+
|
|
96
|
+
for (const op of ops) {
|
|
97
|
+
if (op.op === "create") {
|
|
98
|
+
// Truncate oversized content — the model should stay within the prompt's
|
|
99
|
+
// 500-word limit, but enforce it here as a hard safety net.
|
|
100
|
+
const content =
|
|
101
|
+
op.content.length > MAX_ENTRY_CONTENT_LENGTH
|
|
102
|
+
? op.content.slice(0, MAX_ENTRY_CONTENT_LENGTH) +
|
|
103
|
+
" [truncated — entry too long]"
|
|
104
|
+
: op.content;
|
|
105
|
+
const id = ltm.create({
|
|
106
|
+
projectPath: op.scope === "project" ? input.projectPath : undefined,
|
|
107
|
+
category: op.category,
|
|
108
|
+
title: op.title,
|
|
109
|
+
content,
|
|
110
|
+
session: input.sessionID,
|
|
111
|
+
scope: op.scope,
|
|
112
|
+
crossProject: op.crossProject ?? true,
|
|
113
|
+
});
|
|
114
|
+
idsToSync.push(id);
|
|
115
|
+
created++;
|
|
116
|
+
} else if (op.op === "update") {
|
|
117
|
+
const entry = ltm.get(op.id);
|
|
118
|
+
if (entry) {
|
|
119
|
+
const content =
|
|
120
|
+
op.content !== undefined && op.content.length > MAX_ENTRY_CONTENT_LENGTH
|
|
121
|
+
? op.content.slice(0, MAX_ENTRY_CONTENT_LENGTH) +
|
|
122
|
+
" [truncated — entry too long]"
|
|
123
|
+
: op.content;
|
|
124
|
+
ltm.update(op.id, { content, confidence: op.confidence });
|
|
125
|
+
if (op.content !== undefined) idsToSync.push(op.id);
|
|
126
|
+
updated++;
|
|
127
|
+
}
|
|
128
|
+
} else if (op.op === "delete") {
|
|
129
|
+
const entry = ltm.get(op.id);
|
|
130
|
+
if (entry) {
|
|
131
|
+
ltm.remove(op.id);
|
|
132
|
+
deleted++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Sync cross-references for created/updated entries
|
|
138
|
+
for (const id of idsToSync) {
|
|
139
|
+
ltm.syncRefs(id);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
lastCuratedAt.set(input.sessionID, Date.now());
|
|
143
|
+
return { created, updated, deleted };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function resetCurationTracker(sessionID?: string) {
|
|
147
|
+
if (sessionID) {
|
|
148
|
+
lastCuratedAt.delete(sessionID);
|
|
149
|
+
} else {
|
|
150
|
+
lastCuratedAt.clear();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Consolidation pass: reviews ALL project entries and merges/trims/deletes
|
|
156
|
+
* to reduce entry count to cfg.curator.maxEntries. Only runs when the current
|
|
157
|
+
* entry count exceeds the target. Uses the same worker session as curation.
|
|
158
|
+
*
|
|
159
|
+
* Only "update" and "delete" ops are applied — consolidation never creates entries.
|
|
160
|
+
*/
|
|
161
|
+
export async function consolidate(input: {
|
|
162
|
+
llm: LLMClient;
|
|
163
|
+
projectPath: string;
|
|
164
|
+
sessionID: string;
|
|
165
|
+
model?: { providerID: string; modelID: string };
|
|
166
|
+
}): Promise<{ updated: number; deleted: number }> {
|
|
167
|
+
const cfg = config();
|
|
168
|
+
if (!cfg.curator.enabled) return { updated: 0, deleted: 0 };
|
|
169
|
+
|
|
170
|
+
const entries = ltm.forProject(input.projectPath, false);
|
|
171
|
+
if (entries.length <= cfg.curator.maxEntries) return { updated: 0, deleted: 0 };
|
|
172
|
+
|
|
173
|
+
const entriesForPrompt = entries.map((e) => ({
|
|
174
|
+
id: e.id,
|
|
175
|
+
category: e.category,
|
|
176
|
+
title: e.title,
|
|
177
|
+
content: e.content,
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const userContent = consolidationUser({
|
|
181
|
+
entries: entriesForPrompt,
|
|
182
|
+
targetMax: cfg.curator.maxEntries,
|
|
183
|
+
});
|
|
184
|
+
const model = input.model ?? cfg.model;
|
|
185
|
+
const responseText = await input.llm.prompt(
|
|
186
|
+
CONSOLIDATION_SYSTEM,
|
|
187
|
+
userContent,
|
|
188
|
+
{ model, workerID: "lore-curator" },
|
|
189
|
+
);
|
|
190
|
+
if (!responseText) return { updated: 0, deleted: 0 };
|
|
191
|
+
|
|
192
|
+
const ops = parseOps(responseText);
|
|
193
|
+
let updated = 0;
|
|
194
|
+
let deleted = 0;
|
|
195
|
+
|
|
196
|
+
for (const op of ops) {
|
|
197
|
+
// Consolidation only applies update and delete — never create.
|
|
198
|
+
if (op.op === "update") {
|
|
199
|
+
const entry = ltm.get(op.id);
|
|
200
|
+
if (entry) {
|
|
201
|
+
const content =
|
|
202
|
+
op.content !== undefined && op.content.length > MAX_ENTRY_CONTENT_LENGTH
|
|
203
|
+
? op.content.slice(0, MAX_ENTRY_CONTENT_LENGTH) +
|
|
204
|
+
" [truncated — entry too long]"
|
|
205
|
+
: op.content;
|
|
206
|
+
ltm.update(op.id, { content, confidence: op.confidence });
|
|
207
|
+
updated++;
|
|
208
|
+
}
|
|
209
|
+
} else if (op.op === "delete") {
|
|
210
|
+
const entry = ltm.get(op.id);
|
|
211
|
+
if (entry) {
|
|
212
|
+
ltm.remove(op.id);
|
|
213
|
+
deleted++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// "create" ops are silently ignored — consolidation must not add entries.
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { updated, deleted };
|
|
220
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Bun runtime driver for Lore's SQLite access.
|
|
2
|
+
//
|
|
3
|
+
// Selected automatically via the `#db/driver` subpath import map when running
|
|
4
|
+
// under Bun (OpenCode plugin, `bun test`).
|
|
5
|
+
//
|
|
6
|
+
// The `Database` class is re-exported as-is; `bun:sqlite`'s API already matches
|
|
7
|
+
// everything Lore uses: `.query(sql)` with cached prepared statements, `.run()`,
|
|
8
|
+
// `.all()`, `.get()`, transactions, PRAGMAs, BLOB columns, and FTS5.
|
|
9
|
+
|
|
10
|
+
import { Database } from "bun:sqlite";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
export { Database };
|
|
14
|
+
|
|
15
|
+
/** Stable SHA-256 hex digest — replaces the Bun-only `Bun.CryptoHasher`. */
|
|
16
|
+
export function sha256(input: string): string {
|
|
17
|
+
return createHash("sha256").update(input).digest("hex");
|
|
18
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Node runtime driver for Lore's SQLite access.
|
|
2
|
+
//
|
|
3
|
+
// Selected via the `#db/driver` subpath import map when running under Node
|
|
4
|
+
// (Pi extension, future ACP server, and CI nodes that aren't Bun). `node:sqlite`
|
|
5
|
+
// has shipped in Node since 22.5 and stabilized (no flag) in Node 24.
|
|
6
|
+
//
|
|
7
|
+
// Bun deliberately does NOT implement `node:sqlite`, so src code that imports
|
|
8
|
+
// from this file must go through `#db/driver`. Never import `node:sqlite`
|
|
9
|
+
// directly outside this file — it will break `bun test` which runs against src.
|
|
10
|
+
|
|
11
|
+
import { DatabaseSync, type StatementSync } from "node:sqlite";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Per-database cache of prepared statements keyed by SQL string.
|
|
16
|
+
*
|
|
17
|
+
* `bun:sqlite` automatically caches prepared statements per-DB when using
|
|
18
|
+
* `.query(sql)`; `node:sqlite` has only `.prepare(sql)` which recompiles on
|
|
19
|
+
* every call. We add a thin `.query()` alias on top of `.prepare()` with
|
|
20
|
+
* caching so every existing call site (`db().query(...).all(...)`) keeps
|
|
21
|
+
* working identically.
|
|
22
|
+
*
|
|
23
|
+
* WeakMap: cache is tied to the Database instance lifetime, no manual cleanup.
|
|
24
|
+
*/
|
|
25
|
+
const statementCache = new WeakMap<DatabaseSync, Map<string, StatementSync>>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Drop-in replacement for `bun:sqlite`'s `Database`.
|
|
29
|
+
*
|
|
30
|
+
* Adds a `.query()` method that caches the underlying `StatementSync`
|
|
31
|
+
* per SQL string. All other methods (`.prepare()`, `.exec()`, `.run()`,
|
|
32
|
+
* `.close()`, PRAGMAs, transactions) come from `DatabaseSync` unchanged.
|
|
33
|
+
*/
|
|
34
|
+
export class Database extends DatabaseSync {
|
|
35
|
+
/** Cached prepared statement for this SQL. Compiled on first call. */
|
|
36
|
+
query(sql: string): StatementSync {
|
|
37
|
+
let map = statementCache.get(this);
|
|
38
|
+
if (!map) {
|
|
39
|
+
map = new Map<string, StatementSync>();
|
|
40
|
+
statementCache.set(this, map);
|
|
41
|
+
}
|
|
42
|
+
let stmt = map.get(sql);
|
|
43
|
+
if (!stmt) {
|
|
44
|
+
stmt = this.prepare(sql);
|
|
45
|
+
map.set(sql, stmt);
|
|
46
|
+
}
|
|
47
|
+
return stmt;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Stable SHA-256 hex digest — replaces the Bun-only `Bun.CryptoHasher`. */
|
|
52
|
+
export function sha256(input: string): string {
|
|
53
|
+
return createHash("sha256").update(input).digest("hex");
|
|
54
|
+
}
|