@loreai/core 0.10.2 → 0.11.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/dist/bun/config.d.ts +8 -0
- package/dist/bun/config.d.ts.map +1 -1
- package/dist/bun/db.d.ts.map +1 -1
- package/dist/bun/distillation.d.ts +74 -2
- package/dist/bun/distillation.d.ts.map +1 -1
- package/dist/bun/embedding.d.ts.map +1 -1
- package/dist/bun/gradient.d.ts +72 -0
- package/dist/bun/gradient.d.ts.map +1 -1
- package/dist/bun/index.d.ts +4 -2
- package/dist/bun/index.d.ts.map +1 -1
- package/dist/bun/index.js +530 -67
- package/dist/bun/index.js.map +4 -4
- package/dist/bun/prompt.d.ts +8 -2
- package/dist/bun/prompt.d.ts.map +1 -1
- package/dist/bun/temporal.d.ts +31 -0
- package/dist/bun/temporal.d.ts.map +1 -1
- package/dist/bun/types.d.ts +9 -0
- package/dist/bun/types.d.ts.map +1 -1
- package/dist/bun/worker-model.d.ts +90 -0
- package/dist/bun/worker-model.d.ts.map +1 -0
- package/dist/node/config.d.ts +8 -0
- package/dist/node/config.d.ts.map +1 -1
- package/dist/node/db.d.ts.map +1 -1
- package/dist/node/distillation.d.ts +74 -2
- package/dist/node/distillation.d.ts.map +1 -1
- package/dist/node/embedding.d.ts.map +1 -1
- package/dist/node/gradient.d.ts +72 -0
- package/dist/node/gradient.d.ts.map +1 -1
- package/dist/node/index.d.ts +4 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +530 -67
- package/dist/node/index.js.map +4 -4
- package/dist/node/prompt.d.ts +8 -2
- package/dist/node/prompt.d.ts.map +1 -1
- package/dist/node/temporal.d.ts +31 -0
- package/dist/node/temporal.d.ts.map +1 -1
- package/dist/node/types.d.ts +9 -0
- package/dist/node/types.d.ts.map +1 -1
- package/dist/node/worker-model.d.ts +90 -0
- package/dist/node/worker-model.d.ts.map +1 -0
- package/dist/types/config.d.ts +8 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/distillation.d.ts +74 -2
- package/dist/types/distillation.d.ts.map +1 -1
- package/dist/types/embedding.d.ts.map +1 -1
- package/dist/types/gradient.d.ts +72 -0
- package/dist/types/gradient.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/prompt.d.ts +8 -2
- package/dist/types/prompt.d.ts.map +1 -1
- package/dist/types/temporal.d.ts +31 -0
- package/dist/types/temporal.d.ts.map +1 -1
- package/dist/types/types.d.ts +9 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/worker-model.d.ts +90 -0
- package/dist/types/worker-model.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/config.ts +53 -6
- package/src/db.ts +57 -1
- package/src/distillation.ts +225 -28
- package/src/embedding.ts +7 -0
- package/src/gradient.ts +262 -8
- package/src/index.ts +16 -0
- package/src/lat-reader.ts +4 -4
- package/src/ltm.ts +17 -17
- package/src/prompt.ts +101 -0
- package/src/recall.ts +4 -4
- package/src/temporal.ts +41 -10
- package/src/types.ts +9 -0
- package/src/worker-model.ts +264 -0
package/src/temporal.ts
CHANGED
|
@@ -9,7 +9,38 @@ function estimate(text: string): number {
|
|
|
9
9
|
return Math.ceil(text.length / 3);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Chunk-boundary terminator inserted between chunks by `partsToText`.
|
|
14
|
+
*
|
|
15
|
+
* `\x1f` is ASCII Unit Separator — a non-word control char that:
|
|
16
|
+
* - cannot legitimately appear in normal chat or tool content (control
|
|
17
|
+
* chars are vanishingly rare even in binary file dumps),
|
|
18
|
+
* - is treated as a token separator by FTS5's `unicode61` tokenizer, so
|
|
19
|
+
* it has zero effect on BM25 indexing or scoring,
|
|
20
|
+
* - survives `sanitizeSurrogates()` (which only touches lone UTF-16
|
|
21
|
+
* surrogates, never ASCII control chars).
|
|
22
|
+
*
|
|
23
|
+
* Placed AFTER the existing `\n` so display tools that split on `\n`
|
|
24
|
+
* still render correctly; the structural parser (in `distillation.ts`)
|
|
25
|
+
* splits on `"\n" + CHUNK_TERMINATOR` for unambiguous chunk recovery.
|
|
26
|
+
*
|
|
27
|
+
* Adopted in F3b. Pre-F3b rows are rewritten in-place by a SQL migration
|
|
28
|
+
* (see `db.ts`); after that migration runs, every `temporal_messages.content`
|
|
29
|
+
* value uses this format consistently.
|
|
30
|
+
*/
|
|
31
|
+
export const CHUNK_TERMINATOR = "\x1f";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Serialize a list of message parts into a single content string for the
|
|
35
|
+
* `temporal_messages.content` column. Chunks are separated by
|
|
36
|
+
* `"\n" + CHUNK_TERMINATOR` so the structural parser can recover chunk
|
|
37
|
+
* boundaries unambiguously regardless of payload contents (including
|
|
38
|
+
* payloads that contain literal `[tool:...]` substrings — e.g. when the
|
|
39
|
+
* agent reads a file that documents this very format).
|
|
40
|
+
*
|
|
41
|
+
* Exported so tests can pin producer/consumer round-trip behavior.
|
|
42
|
+
*/
|
|
43
|
+
export function partsToText(parts: LorePart[]): string {
|
|
13
44
|
const chunks: string[] = [];
|
|
14
45
|
for (const part of parts) {
|
|
15
46
|
if (isTextPart(part)) chunks.push(part.text);
|
|
@@ -21,7 +52,7 @@ function partsToText(parts: LorePart[]): string {
|
|
|
21
52
|
// Sanitize unpaired surrogates from tool outputs and other raw text.
|
|
22
53
|
// Without this, surrogates survive into the DB and later break JSON
|
|
23
54
|
// serialization when included in recall tool responses.
|
|
24
|
-
return sanitizeSurrogates(chunks.join("\n"));
|
|
55
|
+
return sanitizeSurrogates(chunks.join("\n" + CHUNK_TERMINATOR));
|
|
25
56
|
}
|
|
26
57
|
|
|
27
58
|
function messageMetadata(info: LoreMessage, parts: LorePart[]): string {
|
|
@@ -167,12 +198,12 @@ export function search(input: {
|
|
|
167
198
|
if (q === EMPTY_QUERY) return [];
|
|
168
199
|
|
|
169
200
|
const ftsSQL = input.sessionID
|
|
170
|
-
? `SELECT m.* FROM
|
|
171
|
-
JOIN
|
|
201
|
+
? `SELECT m.* FROM temporal_fts f
|
|
202
|
+
CROSS JOIN temporal_messages m ON m.rowid = f.rowid
|
|
172
203
|
WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ?
|
|
173
204
|
ORDER BY rank LIMIT ?`
|
|
174
|
-
: `SELECT m.* FROM
|
|
175
|
-
JOIN
|
|
205
|
+
: `SELECT m.* FROM temporal_fts f
|
|
206
|
+
CROSS JOIN temporal_messages m ON m.rowid = f.rowid
|
|
176
207
|
WHERE f.content MATCH ? AND m.project_id = ?
|
|
177
208
|
ORDER BY rank LIMIT ?`;
|
|
178
209
|
const params = input.sessionID
|
|
@@ -222,12 +253,12 @@ export function searchScored(input: {
|
|
|
222
253
|
if (q === EMPTY_QUERY) return [];
|
|
223
254
|
|
|
224
255
|
const ftsSQL = input.sessionID
|
|
225
|
-
? `SELECT m.*, rank FROM
|
|
226
|
-
JOIN
|
|
256
|
+
? `SELECT m.*, rank FROM temporal_fts f
|
|
257
|
+
CROSS JOIN temporal_messages m ON m.rowid = f.rowid
|
|
227
258
|
WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ?
|
|
228
259
|
ORDER BY rank LIMIT ?`
|
|
229
|
-
: `SELECT m.*, rank FROM
|
|
230
|
-
JOIN
|
|
260
|
+
: `SELECT m.*, rank FROM temporal_fts f
|
|
261
|
+
CROSS JOIN temporal_messages m ON m.rowid = f.rowid
|
|
231
262
|
WHERE f.content MATCH ? AND m.project_id = ?
|
|
232
263
|
ORDER BY rank LIMIT ?`;
|
|
233
264
|
const params = input.sessionID
|
package/src/types.ts
CHANGED
|
@@ -35,6 +35,15 @@ export type LoreAssistantMessage = {
|
|
|
35
35
|
modelID: string;
|
|
36
36
|
providerID: string;
|
|
37
37
|
mode: string;
|
|
38
|
+
/**
|
|
39
|
+
* Set to `true` by the OpenCode compaction agent on the assistant
|
|
40
|
+
* message that holds a `/compact` summary (see upstream
|
|
41
|
+
* `compaction.ts:435`). Lore reads this flag in F1b's
|
|
42
|
+
* `findPreviousCompactSummary` to anchor repeat `/compact`
|
|
43
|
+
* invocations to the prior summary. Always undefined for normal
|
|
44
|
+
* assistant turns.
|
|
45
|
+
*/
|
|
46
|
+
summary?: boolean;
|
|
38
47
|
path: { cwd: string; root: string };
|
|
39
48
|
cost: number;
|
|
40
49
|
tokens: {
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic worker model selection.
|
|
3
|
+
*
|
|
4
|
+
* Background workers (distillation, curation, query expansion) don't need
|
|
5
|
+
* frontier reasoning. This module discovers cheaper models from the same
|
|
6
|
+
* provider and validates their quality via a two-phase comparison:
|
|
7
|
+
* Phase 1: structural checks (parsability, observation count, token bounds)
|
|
8
|
+
* Phase 2: LLM judge (session model rates candidate output vs reference)
|
|
9
|
+
*
|
|
10
|
+
* Results are persisted in kv_meta and re-evaluated when the model landscape
|
|
11
|
+
* changes (new models, session model switch, model deprecation).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { db } from "./db";
|
|
15
|
+
import { sha256 } from "#db/driver";
|
|
16
|
+
import * as log from "./log";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** Minimal model info needed for worker selection — provider-agnostic. */
|
|
23
|
+
export type ModelInfo = {
|
|
24
|
+
id: string;
|
|
25
|
+
providerID: string;
|
|
26
|
+
cost: { input: number }; // per-token cost
|
|
27
|
+
status: string;
|
|
28
|
+
capabilities: { input: { text: boolean } };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Result of a worker model validation stored in kv_meta. */
|
|
32
|
+
export type WorkerModelResult = {
|
|
33
|
+
modelID: string;
|
|
34
|
+
providerID: string;
|
|
35
|
+
fingerprint: string;
|
|
36
|
+
validatedAt: number;
|
|
37
|
+
judgeScore: number | null; // null = structural-only (no judge run yet)
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const KV_PREFIX = "lore:worker_model:";
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Candidate selection
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Select worker model candidates from the available models.
|
|
48
|
+
*
|
|
49
|
+
* Returns up to 2 candidates: cheapest overall + one tier below the session
|
|
50
|
+
* model. The session model itself is included (if it's the cheapest, the list
|
|
51
|
+
* has 1 entry and no comparison is needed).
|
|
52
|
+
*/
|
|
53
|
+
export function selectWorkerCandidates(
|
|
54
|
+
sessionModel: { id: string; providerID: string; cost: { input: number } },
|
|
55
|
+
providerModels: ModelInfo[],
|
|
56
|
+
): ModelInfo[] {
|
|
57
|
+
// Filter: same provider, active, text-capable
|
|
58
|
+
const eligible = providerModels.filter(
|
|
59
|
+
(m) =>
|
|
60
|
+
m.providerID === sessionModel.providerID &&
|
|
61
|
+
m.status === "active" &&
|
|
62
|
+
m.capabilities.input.text,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (eligible.length === 0) return [];
|
|
66
|
+
|
|
67
|
+
// Sort by cost ascending (cheapest first)
|
|
68
|
+
const sorted = [...eligible].sort((a, b) => a.cost.input - b.cost.input);
|
|
69
|
+
|
|
70
|
+
// Cheapest overall
|
|
71
|
+
const cheapest = sorted[0];
|
|
72
|
+
|
|
73
|
+
// One tier below session model: the most expensive model that's still
|
|
74
|
+
// cheaper than the session model. If session IS cheapest, this is undefined.
|
|
75
|
+
const belowSession = sorted
|
|
76
|
+
.filter((m) => m.cost.input < sessionModel.cost.input)
|
|
77
|
+
.pop(); // last = most expensive among cheaper ones
|
|
78
|
+
|
|
79
|
+
// Deduplicate
|
|
80
|
+
const candidates = new Map<string, ModelInfo>();
|
|
81
|
+
candidates.set(cheapest.id, cheapest);
|
|
82
|
+
if (belowSession && belowSession.id !== cheapest.id) {
|
|
83
|
+
candidates.set(belowSession.id, belowSession);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If session model is the cheapest, return just it
|
|
87
|
+
if (cheapest.id === sessionModel.id || cheapest.cost.input >= sessionModel.cost.input) {
|
|
88
|
+
return [cheapest];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [...candidates.values()];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Fingerprinting
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Compute a fingerprint from the model landscape. Changes when:
|
|
100
|
+
* - Models are added or removed from the provider
|
|
101
|
+
* - The session model changes
|
|
102
|
+
*/
|
|
103
|
+
export function computeModelFingerprint(
|
|
104
|
+
providerID: string,
|
|
105
|
+
sessionModelID: string,
|
|
106
|
+
activeModelIDs: string[],
|
|
107
|
+
): string {
|
|
108
|
+
const sorted = [...activeModelIDs].sort();
|
|
109
|
+
return sha256(
|
|
110
|
+
JSON.stringify({ providerID, sessionModelID, modelIDs: sorted }),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Persistence
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
export function getValidatedWorkerModel(
|
|
119
|
+
providerID: string,
|
|
120
|
+
): WorkerModelResult | null {
|
|
121
|
+
const row = db()
|
|
122
|
+
.query("SELECT value FROM kv_meta WHERE key = ?")
|
|
123
|
+
.get(`${KV_PREFIX}${providerID}`) as { value: string } | null;
|
|
124
|
+
if (!row) return null;
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(row.value) as WorkerModelResult;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function storeValidatedWorkerModel(result: WorkerModelResult): void {
|
|
133
|
+
const key = `${KV_PREFIX}${result.providerID}`;
|
|
134
|
+
const value = JSON.stringify(result);
|
|
135
|
+
db()
|
|
136
|
+
.query(
|
|
137
|
+
"INSERT INTO kv_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?",
|
|
138
|
+
)
|
|
139
|
+
.run(key, value, value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check whether the stored validation is stale (fingerprint mismatch).
|
|
144
|
+
*/
|
|
145
|
+
export function isValidationStale(
|
|
146
|
+
stored: WorkerModelResult | null,
|
|
147
|
+
currentFingerprint: string,
|
|
148
|
+
): boolean {
|
|
149
|
+
if (!stored) return true;
|
|
150
|
+
return stored.fingerprint !== currentFingerprint;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Structural validation
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
export type StructuralCheckResult = {
|
|
158
|
+
passed: boolean;
|
|
159
|
+
observationCount: number;
|
|
160
|
+
tokenCount: number;
|
|
161
|
+
reason?: string;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Structural quality check: does the candidate distillation output meet
|
|
166
|
+
* minimum quality thresholds relative to the reference?
|
|
167
|
+
*/
|
|
168
|
+
export function structuralCheck(
|
|
169
|
+
candidateObservations: string | null,
|
|
170
|
+
referenceObservations: string,
|
|
171
|
+
): StructuralCheckResult {
|
|
172
|
+
if (candidateObservations == null || candidateObservations.length === 0) {
|
|
173
|
+
return { passed: false, observationCount: 0, tokenCount: 0, reason: candidateObservations === null ? "parse_failed" : "empty" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Count observation lines (non-empty lines starting with common markers)
|
|
177
|
+
const countObs = (text: string) =>
|
|
178
|
+
text.split("\n").filter((l) => l.trim().length > 0).length;
|
|
179
|
+
|
|
180
|
+
const refCount = countObs(referenceObservations);
|
|
181
|
+
const candCount = countObs(candidateObservations);
|
|
182
|
+
const candTokens = Math.ceil(candidateObservations.length / 3);
|
|
183
|
+
|
|
184
|
+
// Observation count within ±50% of reference
|
|
185
|
+
if (refCount > 0 && (candCount < refCount * 0.5 || candCount > refCount * 1.5)) {
|
|
186
|
+
return {
|
|
187
|
+
passed: false,
|
|
188
|
+
observationCount: candCount,
|
|
189
|
+
tokenCount: candTokens,
|
|
190
|
+
reason: `observation_count_${candCount}_vs_ref_${refCount}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Not degenerate: not empty, not >3x reference size
|
|
195
|
+
const refTokens = Math.ceil(referenceObservations.length / 3);
|
|
196
|
+
if (candTokens === 0) {
|
|
197
|
+
return { passed: false, observationCount: candCount, tokenCount: candTokens, reason: "empty" };
|
|
198
|
+
}
|
|
199
|
+
if (refTokens > 0 && candTokens > refTokens * 3) {
|
|
200
|
+
return {
|
|
201
|
+
passed: false,
|
|
202
|
+
observationCount: candCount,
|
|
203
|
+
tokenCount: candTokens,
|
|
204
|
+
reason: `token_count_${candTokens}_vs_ref_${refTokens}_3x`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { passed: true, observationCount: candCount, tokenCount: candTokens };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Judge prompt
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
export const WORKER_JUDGE_SYSTEM = `You are evaluating distillation quality. You will be given a REFERENCE distillation (produced by a capable model) and a CANDIDATE distillation (produced by a cheaper model) of the same conversation segment.
|
|
216
|
+
|
|
217
|
+
Rate the candidate on a scale of 1-5:
|
|
218
|
+
5 = Captures all key facts and decisions, equivalent to reference
|
|
219
|
+
4 = Captures most facts, minor omissions
|
|
220
|
+
3 = Captures the essential facts, some detail loss acceptable
|
|
221
|
+
2 = Missing important facts or technical details
|
|
222
|
+
1 = Significantly incomplete or inaccurate
|
|
223
|
+
|
|
224
|
+
Respond with ONLY a single digit (1-5).`;
|
|
225
|
+
|
|
226
|
+
export function workerJudgeUser(
|
|
227
|
+
reference: string,
|
|
228
|
+
candidate: string,
|
|
229
|
+
): string {
|
|
230
|
+
return `<reference>\n${reference}\n</reference>\n\n<candidate>\n${candidate}\n</candidate>`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Parse the judge's score from a response. Returns null on parse failure. */
|
|
234
|
+
export function parseJudgeScore(response: string): number | null {
|
|
235
|
+
const match = response.trim().match(/^([1-5])/);
|
|
236
|
+
if (!match) return null;
|
|
237
|
+
return parseInt(match[1], 10);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Effective worker model resolution
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolve the effective worker model for a given provider.
|
|
246
|
+
* Priority: explicit config > validated auto-selection > session model (fallback).
|
|
247
|
+
*/
|
|
248
|
+
export function resolveWorkerModel(
|
|
249
|
+
providerID: string,
|
|
250
|
+
configWorkerModel?: { providerID: string; modelID: string },
|
|
251
|
+
configModel?: { providerID: string; modelID: string },
|
|
252
|
+
): { providerID: string; modelID: string } | undefined {
|
|
253
|
+
// Explicit override wins
|
|
254
|
+
if (configWorkerModel) return configWorkerModel;
|
|
255
|
+
|
|
256
|
+
// Check for validated auto-selection
|
|
257
|
+
const validated = getValidatedWorkerModel(providerID);
|
|
258
|
+
if (validated) {
|
|
259
|
+
return { providerID: validated.providerID, modelID: validated.modelID };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fall back to the session model config (or undefined = host default)
|
|
263
|
+
return configModel;
|
|
264
|
+
}
|