@unblocklabs/unblock-memory 0.3.29 → 0.4.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/README.md +2 -2
- package/dist/src/config.d.ts +0 -1
- package/dist/src/config.js +2 -6
- package/dist/src/diagnostics.d.ts +1 -3
- package/dist/src/manager.d.ts +4 -8
- package/dist/src/manager.js +17 -45
- package/dist/src/memory-passage.d.ts +26 -0
- package/dist/src/memory-passage.js +48 -0
- package/dist/src/memory-whisperer.d.ts +2 -1
- package/dist/src/memory-whisperer.js +21 -17
- package/dist/src/mlx-query.d.ts +3 -10
- package/dist/src/mlx-query.js +49 -25
- package/dist/src/query-contract.d.ts +31 -0
- package/dist/src/query-contract.js +58 -0
- package/dist/src/query-tokenizer/contract.json +10 -0
- package/dist/src/response-stages.d.ts +3 -3
- package/dist/src/training-candidates.d.ts +2 -1
- package/dist/src/training-candidates.js +12 -9
- package/dist/src/training-input.d.ts +13 -10
- package/dist/src/training-input.js +22 -18
- package/dist/src/training-judge.d.ts +16 -46
- package/dist/src/training-judge.js +6 -53
- package/dist/src/training-models.d.ts +15 -7
- package/dist/src/training-models.js +36 -38
- package/dist/src/training-queries.d.ts +23 -7
- package/dist/src/training-queries.js +247 -102
- package/dist/src/training-retrieval.d.ts +9 -2
- package/dist/src/training-retrieval.js +36 -12
- package/dist/src/training-runtime.js +8 -7
- package/dist/src/training-store.d.ts +46 -16
- package/dist/src/training-store.js +83 -29
- package/dist/src/training.d.ts +10 -0
- package/dist/src/training.js +7 -5
- package/dist/src/typesafe.d.ts +58 -8
- package/dist/src/typesafe.js +47 -29
- package/dist/src/whisperer-context.d.ts +0 -8
- package/dist/src/whisperer-context.js +0 -23
- package/docs/configuration.md +3 -3
- package/docs/memory-training.md +114 -58
- package/docs/retrieval.md +34 -24
- package/openclaw.plugin.json +2 -3
- package/package.json +5 -2
- package/src/mlx-query-worker.py +5 -8
- package/src/query-tokenizer/LICENSE +71 -0
- package/src/query-tokenizer/README.md +21 -0
- package/src/query-tokenizer/chat_template.jinja +115 -0
- package/src/query-tokenizer/contract.json +10 -0
- package/src/query-tokenizer/tokenizer.json +323830 -0
- package/src/query-tokenizer/tokenizer_config.json +19 -0
- package/src/query_contract.py +60 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
|
|
6
6
|
import { readSessionManifest } from "./session-sync.js";
|
|
7
7
|
import { resolveSessionSource } from "./sources.js";
|
|
8
8
|
import { trainingCandidates } from "./training-candidates.js";
|
|
9
|
-
|
|
9
|
+
import { MEMORY_PASSAGE_VERSION, renderMemoryPassage, duplicateMemoryPassage } from "./memory-passage.js";
|
|
10
|
+
export const TRAINING_RETRIEVAL_VERSION = `qmd-2.10.2-historical-lanes-depth10-v3:${MEMORY_PASSAGE_VERSION}`;
|
|
10
11
|
export const TRAINING_SEARCH_OPTIONS = { vector: 10, bm25: 10, mergedLimit: null, rerank: false };
|
|
12
|
+
/** Missing historical evidence is reviewable; unexpected SQLite/I/O failures remain fatal. */
|
|
13
|
+
export class HistoricalCorpusUnavailableError extends Error {
|
|
14
|
+
}
|
|
11
15
|
/** Never infer dates by parsing message bodies: headings can be quoted or forged. */
|
|
12
16
|
export function historicalPrefix(body, spans, cutoff) {
|
|
13
17
|
if (!spans?.length || !body.startsWith("# Transcript\n\n") || !Number.isFinite(cutoff))
|
|
@@ -47,8 +51,15 @@ export function historicalPrefix(body, spans, cutoff) {
|
|
|
47
51
|
* No filesystem projection, live-index mutation, model re-embedding or dependency patch. */
|
|
48
52
|
export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, openStore) {
|
|
49
53
|
const indexPath = join(stateDir, "index.sqlite");
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
try {
|
|
55
|
+
statSync(indexPath);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
59
|
+
throw new HistoricalCorpusUnavailableError("No QMD index; sync sessions before evaluating training queries");
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
52
63
|
const manifest = await readSessionManifest(join(stateDir, "sessions-manifest.json"));
|
|
53
64
|
const source = resolveSessionSource(join(stateDir, "sessions"), chatTypes);
|
|
54
65
|
const createStore = openStore ?? (await import("@unblocklabs/qmd")).createStore;
|
|
@@ -189,16 +200,21 @@ export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, open
|
|
|
189
200
|
}
|
|
190
201
|
};
|
|
191
202
|
return { corpusHash, report, maxDate,
|
|
192
|
-
search: async (query) => {
|
|
203
|
+
search: async (query, lane) => {
|
|
193
204
|
if (closed)
|
|
194
205
|
throw new Error("Historical snapshot is closed");
|
|
195
206
|
if (!report.sessions)
|
|
196
207
|
return [];
|
|
197
|
-
if (!report.chunks)
|
|
198
|
-
throw new
|
|
208
|
+
if (lane === "vec" && !report.chunks)
|
|
209
|
+
throw new HistoricalCorpusUnavailableError("Historical snapshot has no vectors; refusing an unresolved vector evaluation");
|
|
199
210
|
const qmd = await (indexed ??= materialize());
|
|
200
|
-
const hits = await trainingCandidates(qmd, query, source.collection,
|
|
201
|
-
|
|
211
|
+
const hits = await trainingCandidates(qmd, query, source.collection, lane);
|
|
212
|
+
const rendered = await Promise.all(hits.map(hit => trainingHit(hit, metadata, cutoff)));
|
|
213
|
+
const distinct = [];
|
|
214
|
+
for (const hit of rendered)
|
|
215
|
+
if (hit && !duplicateMemoryPassage(hit, distinct))
|
|
216
|
+
distinct.push(hit);
|
|
217
|
+
return distinct;
|
|
202
218
|
}, close: async () => {
|
|
203
219
|
if (closed)
|
|
204
220
|
return;
|
|
@@ -221,12 +237,20 @@ function historicalChunk(chunk, length) {
|
|
|
221
237
|
return Number.isSafeInteger(chunk.pos) && Number.isSafeInteger(chunk.chunk_len) && chunk.pos >= 0 && chunk.chunk_len > 0 &&
|
|
222
238
|
chunk.pos + chunk.chunk_len <= length;
|
|
223
239
|
}
|
|
224
|
-
function trainingHit(hit, metadata, cutoff) {
|
|
225
|
-
const
|
|
240
|
+
async function trainingHit(hit, metadata, cutoff) {
|
|
241
|
+
const messages = metadata.get(hit.file);
|
|
242
|
+
const selected = await renderMemoryPassage({ body: hit.body, bestChunk: hit.bestChunk,
|
|
243
|
+
chunkPos: hit.bestChunkPos, chunkLen: hit.bestChunk.length }, messages);
|
|
244
|
+
if (!selected)
|
|
245
|
+
return;
|
|
246
|
+
const spans = messages?.filter(s => s.start < selected.position + (selected.sourceText ?? selected.text).length && s.end > selected.position);
|
|
226
247
|
if (!spans?.length || spans.some(s => !(Date.parse(s.timestamp) < Math.floor(cutoff / 1000) * 1000)) ||
|
|
227
248
|
hit.body.slice(hit.bestChunkPos, hit.bestChunkPos + hit.bestChunk.length) !== hit.bestChunk) {
|
|
228
249
|
throw new Error("QMD returned evidence outside the historical snapshot");
|
|
229
250
|
}
|
|
230
|
-
|
|
251
|
+
const startLine = hit.body.slice(0, selected.position).split("\n").length;
|
|
252
|
+
const endLine = startLine + (selected.sourceText ?? selected.text).split("\n").length - 1;
|
|
253
|
+
return { path: hit.file, corpus: "sessions", position: selected.position, text: selected.text, startLine, endLine,
|
|
254
|
+
dates: [...new Set(spans.map(s => s.timestamp))],
|
|
231
255
|
score: hit.score, methods: hit.explain?.methods ?? [] };
|
|
232
256
|
}
|
|
@@ -66,7 +66,7 @@ export function registerMemoryTraining(api, config) {
|
|
|
66
66
|
process.exitCode = 1;
|
|
67
67
|
});
|
|
68
68
|
for (const command of ["generate", "evaluate"]) {
|
|
69
|
-
const stage = root.command(command).description(command === "generate" ? "Generate
|
|
69
|
+
const stage = root.command(command).description(command === "generate" ? "Generate five candidates per lex/vec lane with isolated gpt-6-luna" : "Grade, revise once per lane and select independent lex/vec winners")
|
|
70
70
|
.option("--agent <id>", "Agent id", "main")
|
|
71
71
|
.option("--threshold <p>", "Minimum completed recall probability", String(TRAINING_GATE_THRESHOLD))
|
|
72
72
|
.option("--max-examples <n>", "Optional maximum uncached examples")
|
|
@@ -75,18 +75,18 @@ export function registerMemoryTraining(api, config) {
|
|
|
75
75
|
stage.option("--max-input-bytes <n>", "Teacher input byte budget", "3000000")
|
|
76
76
|
.option("--concurrency <n>", "Concurrent isolated teacher completions", "8");
|
|
77
77
|
else
|
|
78
|
-
stage.option("--max-calls <n>", "Maximum new retrieval operations
|
|
79
|
-
.option("--
|
|
78
|
+
stage.option("--max-calls <n>", "Maximum new teacher completions, retrieval operations and passage judgments")
|
|
79
|
+
.option("--max-input-bytes <n>", "Revision teacher input byte budget", "3000000")
|
|
80
80
|
.option("--concurrency <n>", "Concurrent historical inputs; remote passage judgments run concurrently", String(TRAINING_EVALUATION_CONCURRENCY));
|
|
81
81
|
stage.action(async (opts) => {
|
|
82
82
|
const options = { maxExamples: opts.maxExamples === undefined ? undefined : Number(opts.maxExamples),
|
|
83
83
|
dryRun: opts.dryRun, threshold: thresholdOption(opts.threshold) };
|
|
84
84
|
const result = await withStore(opts.agent, {}, async (store, source) => command === "generate"
|
|
85
85
|
? await generateTrainingQueries(source, store, api.runtime, { ...options, maxInputBytes: Number(opts.maxInputBytes), concurrency: Number(opts.concurrency) })
|
|
86
|
-
: await evaluateTrainingQueries(source, store, config, { ...options, maxCalls: opts.maxCalls === undefined ? undefined : Number(opts.maxCalls),
|
|
87
|
-
concurrency: Number(opts.concurrency),
|
|
86
|
+
: await evaluateTrainingQueries(source, store, config, api.runtime, { ...options, maxCalls: opts.maxCalls === undefined ? undefined : Number(opts.maxCalls),
|
|
87
|
+
concurrency: Number(opts.concurrency), maxInputBytes: Number(opts.maxInputBytes) }));
|
|
88
88
|
console.log(JSON.stringify(result, null, 2));
|
|
89
|
-
if (result.failed || result.ambiguous || result.blocked)
|
|
89
|
+
if (result.failed || result.ambiguous || result.blocked || result.flagged)
|
|
90
90
|
process.exitCode = 1;
|
|
91
91
|
});
|
|
92
92
|
}
|
|
@@ -97,9 +97,10 @@ export function registerMemoryTraining(api, config) {
|
|
|
97
97
|
console.log(JSON.stringify(await withStore(opts.agent, { readOnly: true }, store => store.status(threshold)), null, 2));
|
|
98
98
|
});
|
|
99
99
|
root.command("retry-failed").option("--agent <id>", "Agent id", "main")
|
|
100
|
+
.requiredOption("--id <hash...>", "Exact reviewed recall-gate or query-step hashes from status")
|
|
100
101
|
.option("--include-ambiguous", "Explicitly permit retrying requests that may already have been billed")
|
|
101
102
|
.action(async (opts) => {
|
|
102
|
-
const reset = await withStore(opts.agent, {}, store => store.retry(opts.includeAmbiguous === true));
|
|
103
|
+
const reset = await withStore(opts.agent, {}, store => store.retry(opts.includeAmbiguous === true, opts.id));
|
|
103
104
|
console.log(JSON.stringify({ reset, calls: 0 }));
|
|
104
105
|
});
|
|
105
106
|
root.command("export").option("--agent <id>", "Agent id", "main")
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type TrainingExample, type TrainingInput } from "./training-input.js";
|
|
2
2
|
import { type judgeTrainingInput } from "./training-gate.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type TeacherResult, type TrainingRound } from "./training-models.js";
|
|
4
4
|
import type { TrainingHit } from "./training-retrieval.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { judgeTrainingPassage } from "./training-judge.js";
|
|
6
|
+
import type { QueryLane, QueryPair } from "./query-contract.js";
|
|
6
7
|
type GateResult = Awaited<ReturnType<typeof judgeTrainingInput>>;
|
|
7
8
|
type Job = {
|
|
8
9
|
id: string;
|
|
@@ -18,19 +19,19 @@ export type TrainingSourceExample = {
|
|
|
18
19
|
};
|
|
19
20
|
export type QueryEvaluation = {
|
|
20
21
|
query: string;
|
|
22
|
+
lane: QueryLane;
|
|
23
|
+
round: TrainingRound;
|
|
21
24
|
retrievalId: string;
|
|
22
25
|
score: number;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
excluded: boolean;
|
|
26
|
-
}[];
|
|
26
|
+
maxProbability: number;
|
|
27
|
+
judgments: string[];
|
|
27
28
|
};
|
|
28
29
|
type TrainingEvaluation = {
|
|
29
30
|
sourceId: string;
|
|
30
31
|
inputHash: string;
|
|
31
32
|
timestamp: number;
|
|
32
33
|
corpusHash: string;
|
|
33
|
-
|
|
34
|
+
teacherIds: string[];
|
|
34
35
|
corpusReport: {
|
|
35
36
|
sessions: number;
|
|
36
37
|
chunks: number;
|
|
@@ -39,23 +40,31 @@ type TrainingEvaluation = {
|
|
|
39
40
|
excludedChunks: number;
|
|
40
41
|
};
|
|
41
42
|
queries: QueryEvaluation[];
|
|
42
|
-
selected:
|
|
43
|
+
selected: QueryPair | null;
|
|
44
|
+
review: string[];
|
|
43
45
|
};
|
|
44
46
|
export type TrainingStepResults = {
|
|
45
47
|
generate: TeacherResult;
|
|
46
48
|
retrieve: {
|
|
47
49
|
query: string;
|
|
50
|
+
lane: QueryLane;
|
|
48
51
|
maxDate: string;
|
|
49
52
|
corpusHash: string;
|
|
50
53
|
hits: TrainingHit[];
|
|
51
54
|
};
|
|
52
|
-
judge: ReturnType<typeof
|
|
53
|
-
|
|
54
|
-
reason: "operator-exclusion";
|
|
55
|
-
};
|
|
55
|
+
judge: Awaited<ReturnType<typeof judgeTrainingPassage>>;
|
|
56
|
+
score: QueryEvaluation;
|
|
56
57
|
evaluate: TrainingEvaluation;
|
|
57
58
|
};
|
|
58
59
|
type StepStatus = "pending" | "attempted" | "complete" | "failed" | "ambiguous";
|
|
60
|
+
type ReviewDetails = {
|
|
61
|
+
steps: string[];
|
|
62
|
+
} | {
|
|
63
|
+
lanes: string[];
|
|
64
|
+
evaluationId: string;
|
|
65
|
+
} | {
|
|
66
|
+
timestamp: number;
|
|
67
|
+
};
|
|
59
68
|
export declare class TrainingStore {
|
|
60
69
|
#private;
|
|
61
70
|
constructor(path: string, agentId: string);
|
|
@@ -75,7 +84,7 @@ export declare class TrainingStore {
|
|
|
75
84
|
status: "failed" | "ambiguous";
|
|
76
85
|
error: string;
|
|
77
86
|
}): void;
|
|
78
|
-
retry(includeAmbiguous: boolean): number;
|
|
87
|
+
retry(includeAmbiguous: boolean, ids: readonly string[]): number;
|
|
79
88
|
activeExamples(): TrainingSourceExample[];
|
|
80
89
|
queryExamples(threshold?: number): {
|
|
81
90
|
recallProbability: number;
|
|
@@ -85,14 +94,24 @@ export declare class TrainingStore {
|
|
|
85
94
|
sessionId: string;
|
|
86
95
|
timestamp: number;
|
|
87
96
|
}[];
|
|
88
|
-
step<S extends keyof TrainingStepResults>(stage: S,
|
|
97
|
+
step<S extends keyof TrainingStepResults>(stage: S, parameters: Record<string, unknown>): {
|
|
89
98
|
id: string;
|
|
90
99
|
stage: S;
|
|
91
|
-
request:
|
|
100
|
+
request: {
|
|
101
|
+
recipe: string;
|
|
102
|
+
};
|
|
92
103
|
status: StepStatus;
|
|
93
104
|
result: TrainingStepResults[S] | undefined;
|
|
94
105
|
};
|
|
95
|
-
|
|
106
|
+
flagReview(example: TrainingSourceExample, reason: string, details: ReviewDetails): void;
|
|
107
|
+
clearReview(sourceId: string): void;
|
|
108
|
+
reviews(): {
|
|
109
|
+
sourceId: string;
|
|
110
|
+
inputHash: string;
|
|
111
|
+
reason: string;
|
|
112
|
+
details: ReviewDetails;
|
|
113
|
+
updatedAt: number;
|
|
114
|
+
}[];
|
|
96
115
|
startStep(stage: keyof TrainingStepResults, id: string, request: unknown): number;
|
|
97
116
|
finishStep<S extends keyof TrainingStepResults>(stage: S, id: string, attempt: number, outcome: {
|
|
98
117
|
result: TrainingStepResults[S];
|
|
@@ -103,6 +122,7 @@ export declare class TrainingStore {
|
|
|
103
122
|
completedEvaluations(versions?: {
|
|
104
123
|
selection: string;
|
|
105
124
|
retrieval: string;
|
|
125
|
+
judge: string;
|
|
106
126
|
}): TrainingEvaluation[];
|
|
107
127
|
sourceDetails(id: string): {
|
|
108
128
|
nodeId: string;
|
|
@@ -116,6 +136,7 @@ export declare class TrainingStore {
|
|
|
116
136
|
result: unknown;
|
|
117
137
|
completedAt: import("node:sqlite").SQLOutputValue;
|
|
118
138
|
};
|
|
139
|
+
stepRecordStatus(id: string): StepStatus | undefined;
|
|
119
140
|
status(threshold: number): {
|
|
120
141
|
nodeId: string;
|
|
121
142
|
agentId: string;
|
|
@@ -132,6 +153,15 @@ export declare class TrainingStore {
|
|
|
132
153
|
inputTokens: number;
|
|
133
154
|
outputTokens: number;
|
|
134
155
|
negative: number;
|
|
156
|
+
recipe: string;
|
|
157
|
+
reviews: {
|
|
158
|
+
sourceId: string;
|
|
159
|
+
inputHash: string;
|
|
160
|
+
reason: string;
|
|
161
|
+
details: ReviewDetails;
|
|
162
|
+
updatedAt: number;
|
|
163
|
+
}[];
|
|
164
|
+
retryable: Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
135
165
|
queryStages: Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
136
166
|
queryAttempts: Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
137
167
|
attempts: Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { openMemoryDatabase } from "./memory-database.js";
|
|
3
3
|
import { trainingHash, TRAINING_PREPARATION } from "./training-input.js";
|
|
4
4
|
import { TRAINING_GATE_MODEL, TRAINING_GATE_THRESHOLD, TRAINING_GATE_VERSION } from "./training-gate.js";
|
|
5
|
+
import { TRAINING_RECIPE_VERSION } from "./training-models.js";
|
|
5
6
|
const LEASE_MS = 120_000;
|
|
6
7
|
export class TrainingStore {
|
|
7
8
|
#db;
|
|
@@ -43,8 +44,10 @@ export class TrainingStore {
|
|
|
43
44
|
status TEXT NOT NULL CHECK(status IN ('pending','attempted','complete','failed','ambiguous')),
|
|
44
45
|
result_json TEXT, error TEXT, completed_at INTEGER
|
|
45
46
|
) STRICT;
|
|
46
|
-
CREATE
|
|
47
|
-
|
|
47
|
+
CREATE TABLE IF NOT EXISTS training_reviews (
|
|
48
|
+
source_id TEXT PRIMARY KEY REFERENCES training_examples(id), input_hash TEXT NOT NULL,
|
|
49
|
+
recipe TEXT NOT NULL, reason TEXT NOT NULL, details_json TEXT NOT NULL, updated_at INTEGER NOT NULL
|
|
50
|
+
) STRICT;
|
|
48
51
|
CREATE TABLE IF NOT EXISTS training_step_attempts (
|
|
49
52
|
id INTEGER PRIMARY KEY, step_id TEXT NOT NULL REFERENCES training_steps(id), started_at INTEGER NOT NULL,
|
|
50
53
|
finished_at INTEGER, status TEXT NOT NULL, error TEXT, result_json TEXT
|
|
@@ -87,10 +90,17 @@ export class TrainingStore {
|
|
|
87
90
|
if (!lock.changes)
|
|
88
91
|
throw new Error("Another memory-training command is running");
|
|
89
92
|
// A request may have been billed before a crashed process saved the response.
|
|
90
|
-
this.#db.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
this.#db.prepare(`UPDATE training_gates SET status='ambiguous',error='interrupted' WHERE status='attempted'
|
|
94
|
+
AND prompt_version=? AND requested_model=? AND input_hash IN (SELECT hash FROM training_inputs WHERE preparation=?)`)
|
|
95
|
+
.run(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
|
|
96
|
+
this.#db.prepare(`UPDATE training_attempts SET status='ambiguous',error='interrupted' WHERE status='attempted'
|
|
97
|
+
AND gate_id IN (SELECT g.id FROM training_gates g JOIN training_inputs i ON i.hash=g.input_hash
|
|
98
|
+
WHERE g.prompt_version=? AND g.requested_model=? AND i.preparation=?)`)
|
|
99
|
+
.run(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
|
|
100
|
+
this.#db.prepare(`UPDATE training_steps SET status='ambiguous',error='interrupted' WHERE status='attempted'
|
|
101
|
+
AND json_extract(request_json,'$.recipe')=?`).run(TRAINING_RECIPE_VERSION);
|
|
102
|
+
this.#db.prepare(`UPDATE training_step_attempts SET status='ambiguous',error='interrupted' WHERE status='attempted'
|
|
103
|
+
AND step_id IN (SELECT id FROM training_steps WHERE json_extract(request_json,'$.recipe')=?)`).run(TRAINING_RECIPE_VERSION);
|
|
94
104
|
});
|
|
95
105
|
let leaseError;
|
|
96
106
|
const heartbeat = setInterval(() => { try {
|
|
@@ -156,13 +166,14 @@ export class TrainingStore {
|
|
|
156
166
|
this.#db.prepare("UPDATE training_examples SET active=-1 WHERE session_id=? AND active=1").run(sessionId);
|
|
157
167
|
}
|
|
158
168
|
#scope = `g.prompt_version=? AND g.requested_model=? AND EXISTS
|
|
159
|
-
(SELECT 1 FROM training_examples e
|
|
169
|
+
(SELECT 1 FROM training_examples e JOIN training_inputs i ON i.hash=e.input_hash
|
|
170
|
+
WHERE e.input_hash=g.input_hash AND e.active=1 AND i.preparation=?)`;
|
|
160
171
|
pending(limit) {
|
|
161
172
|
return this.#db.prepare(`SELECT g.id,g.input_hash inputHash,i.input_json inputJson
|
|
162
173
|
FROM training_gates g JOIN training_inputs i ON i.hash=g.input_hash
|
|
163
174
|
WHERE ${this.#scope} AND g.status='pending' ORDER BY
|
|
164
175
|
(SELECT MAX(timestamp) FROM training_examples WHERE input_hash=g.input_hash AND active=1) DESC,g.id LIMIT ?`)
|
|
165
|
-
.all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, limit ?? -1);
|
|
176
|
+
.all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION, limit ?? -1);
|
|
166
177
|
}
|
|
167
178
|
start(id) {
|
|
168
179
|
return this.#transaction(() => {
|
|
@@ -185,35 +196,61 @@ export class TrainingStore {
|
|
|
185
196
|
.run(status, Date.now(), error, ok ? JSON.stringify(result) : null, attempt, id);
|
|
186
197
|
});
|
|
187
198
|
}
|
|
188
|
-
retry(includeAmbiguous) {
|
|
199
|
+
retry(includeAmbiguous, ids) {
|
|
200
|
+
if (!ids.length || ids.some(id => !/^[a-f0-9]{64}$/u.test(id)))
|
|
201
|
+
throw new Error("Specify exact training gate or step hashes to retry");
|
|
189
202
|
this.#checkLease();
|
|
190
|
-
return this.#transaction(() =>
|
|
203
|
+
return this.#transaction(() => ids.reduce((count, id) => {
|
|
204
|
+
const statuses = includeAmbiguous ? "('failed','ambiguous')" : "('failed')";
|
|
205
|
+
const gates = this.#db.prepare(`UPDATE training_gates SET status='pending',error=NULL WHERE id=?
|
|
206
|
+
AND prompt_version=? AND requested_model=? AND status IN ${statuses}
|
|
207
|
+
AND input_hash IN (SELECT hash FROM training_inputs WHERE preparation=?)`)
|
|
208
|
+
.run(id, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
|
|
209
|
+
const steps = this.#db.prepare(`UPDATE training_steps SET status='pending',error=NULL WHERE id=?
|
|
210
|
+
AND json_extract(request_json,'$.recipe')=? AND status IN ${statuses}`).run(id, TRAINING_RECIPE_VERSION);
|
|
211
|
+
return count + Number(gates.changes) + Number(steps.changes);
|
|
212
|
+
}, 0));
|
|
191
213
|
}
|
|
192
214
|
activeExamples() {
|
|
193
215
|
return this.#db.prepare(`SELECT e.id,e.input_hash inputHash,i.input_json inputJson,e.session_id sessionId,e.timestamp
|
|
194
216
|
FROM training_examples e JOIN training_inputs i ON i.hash=e.input_hash
|
|
195
|
-
WHERE e.active=1 ORDER BY e.timestamp DESC,e.id`).all();
|
|
217
|
+
WHERE e.active=1 AND i.preparation=? ORDER BY e.timestamp DESC,e.id`).all(TRAINING_PREPARATION);
|
|
196
218
|
}
|
|
197
219
|
queryExamples(threshold = TRAINING_GATE_THRESHOLD) {
|
|
198
220
|
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1)
|
|
199
221
|
throw new Error("Threshold must be between 0 and 1");
|
|
200
222
|
const probabilities = new Map(this.#db.prepare(`SELECT g.input_hash,g.probability FROM training_gates g
|
|
201
223
|
WHERE ${this.#scope} AND g.status='complete' AND g.probability>=?`)
|
|
202
|
-
.all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, threshold).map(row => [String(row.input_hash), Number(row.probability)]));
|
|
224
|
+
.all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION, threshold).map(row => [String(row.input_hash), Number(row.probability)]));
|
|
203
225
|
return this.activeExamples().flatMap(example => {
|
|
204
226
|
const recallProbability = probabilities.get(example.inputHash);
|
|
205
227
|
return recallProbability === undefined ? [] : [{ ...example, recallProbability }];
|
|
206
228
|
});
|
|
207
229
|
}
|
|
208
|
-
step(stage,
|
|
230
|
+
step(stage, parameters) {
|
|
231
|
+
const request = { ...parameters, recipe: TRAINING_RECIPE_VERSION };
|
|
209
232
|
const id = trainingHash([stage, request]);
|
|
210
233
|
const row = this.#db.prepare("SELECT status,result_json FROM training_steps WHERE id=? AND stage=?").get(id, stage);
|
|
211
234
|
return { id, stage, request, status: (row?.status ?? "pending"),
|
|
212
235
|
result: row?.status === "complete" ? JSON.parse(String(row.result_json)) : undefined };
|
|
213
236
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
flagReview(example, reason, details) {
|
|
238
|
+
this.#checkLease();
|
|
239
|
+
this.#db.prepare(`INSERT INTO training_reviews VALUES (?,?,?,?,?,?) ON CONFLICT(source_id) DO UPDATE SET
|
|
240
|
+
input_hash=excluded.input_hash,recipe=excluded.recipe,reason=excluded.reason,
|
|
241
|
+
details_json=excluded.details_json,updated_at=excluded.updated_at`)
|
|
242
|
+
.run(example.id, example.inputHash, TRAINING_RECIPE_VERSION, reason, JSON.stringify(details), Date.now());
|
|
243
|
+
}
|
|
244
|
+
clearReview(sourceId) {
|
|
245
|
+
this.#checkLease();
|
|
246
|
+
this.#db.prepare("DELETE FROM training_reviews WHERE source_id=? AND recipe=?").run(sourceId, TRAINING_RECIPE_VERSION);
|
|
247
|
+
}
|
|
248
|
+
reviews() {
|
|
249
|
+
return this.#db.prepare(`SELECT r.source_id sourceId,r.input_hash inputHash,r.reason,r.details_json details,r.updated_at updatedAt
|
|
250
|
+
FROM training_reviews r JOIN training_examples e ON e.id=r.source_id
|
|
251
|
+
WHERE r.recipe=? AND e.active=1 AND e.input_hash=r.input_hash ORDER BY r.updated_at DESC,r.source_id`)
|
|
252
|
+
.all(TRAINING_RECIPE_VERSION).map(row => ({ sourceId: String(row.sourceId), inputHash: String(row.inputHash),
|
|
253
|
+
reason: String(row.reason), details: JSON.parse(String(row.details)), updatedAt: Number(row.updatedAt) }));
|
|
217
254
|
}
|
|
218
255
|
startStep(stage, id, request) {
|
|
219
256
|
return this.#transaction(() => {
|
|
@@ -242,8 +279,10 @@ export class TrainingStore {
|
|
|
242
279
|
}
|
|
243
280
|
completedEvaluations(versions) {
|
|
244
281
|
return this.#db.prepare(`SELECT result_json FROM training_steps WHERE stage='evaluate' AND status='complete'
|
|
245
|
-
AND
|
|
246
|
-
|
|
282
|
+
AND json_extract(request_json,'$.recipe')=?
|
|
283
|
+
AND (? IS NULL OR (json_extract(request_json,'$.version')=? AND json_extract(request_json,'$.retrievalVersion')=?
|
|
284
|
+
AND json_extract(request_json,'$.judgeVersion')=?)) ORDER BY completed_at DESC,id`)
|
|
285
|
+
.all(TRAINING_RECIPE_VERSION, versions?.selection ?? null, versions?.selection ?? null, versions?.retrieval ?? null, versions?.judge ?? null)
|
|
247
286
|
.map(row => JSON.parse(String(row.result_json)));
|
|
248
287
|
}
|
|
249
288
|
sourceDetails(id) {
|
|
@@ -260,34 +299,49 @@ export class TrainingStore {
|
|
|
260
299
|
return { id, stage: row.stage, request: JSON.parse(String(row.request_json)),
|
|
261
300
|
result: JSON.parse(String(row.result_json)), completedAt: row.completed_at };
|
|
262
301
|
}
|
|
302
|
+
stepRecordStatus(id) {
|
|
303
|
+
return this.#db.prepare("SELECT status FROM training_steps WHERE id=? AND json_extract(request_json,'$.recipe')=?")
|
|
304
|
+
.get(id, TRAINING_RECIPE_VERSION)?.status;
|
|
305
|
+
}
|
|
263
306
|
status(threshold) {
|
|
264
307
|
const stages = this.#db.prepare(`SELECT g.status,COUNT(*) count FROM training_gates g WHERE ${this.#scope} GROUP BY g.status`)
|
|
265
|
-
.all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
|
|
308
|
+
.all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
|
|
266
309
|
const labels = this.#db.prepare(`SELECT COUNT(*) complete,COALESCE(SUM(g.probability>=?),0) positive,
|
|
267
310
|
COALESCE(SUM(g.input_tokens),0) inputTokens,COALESCE(SUM(g.output_tokens),0) outputTokens
|
|
268
|
-
FROM training_gates g WHERE ${this.#scope} AND g.status='complete'`).get(threshold, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
|
|
311
|
+
FROM training_gates g WHERE ${this.#scope} AND g.status='complete'`).get(threshold, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
|
|
269
312
|
return { nodeId: this.#nodeId, agentId: this.#agentId, preparation: TRAINING_PREPARATION,
|
|
270
313
|
promptVersion: TRAINING_GATE_VERSION, requestedModel: TRAINING_GATE_MODEL, threshold,
|
|
271
|
-
examples: this.#db.prepare(
|
|
272
|
-
|
|
314
|
+
examples: this.#db.prepare(`SELECT e.active,COUNT(*) count FROM training_examples e
|
|
315
|
+
JOIN training_inputs i ON i.hash=e.input_hash WHERE i.preparation=? GROUP BY e.active`).all(TRAINING_PREPARATION),
|
|
316
|
+
collectedInputs: Number(this.#db.prepare(`SELECT COUNT(DISTINCT e.input_hash) count FROM training_examples e
|
|
317
|
+
JOIN training_inputs i ON i.hash=e.input_hash WHERE e.active=1 AND i.preparation=?`).get(TRAINING_PREPARATION).count),
|
|
273
318
|
queryInputs: Number(labels.positive),
|
|
274
319
|
stages, complete: Number(labels.complete), positive: Number(labels.positive),
|
|
275
320
|
inputTokens: Number(labels.inputTokens), outputTokens: Number(labels.outputTokens),
|
|
276
321
|
negative: Number(labels.complete) - Number(labels.positive),
|
|
277
|
-
|
|
322
|
+
recipe: TRAINING_RECIPE_VERSION, reviews: this.reviews(),
|
|
323
|
+
retryable: this.#db.prepare(`SELECT id,stage,status,error FROM training_steps
|
|
324
|
+
WHERE json_extract(request_json,'$.recipe')=? AND status IN ('failed','ambiguous')
|
|
325
|
+
UNION ALL SELECT g.id,'recall',g.status,g.error FROM training_gates g WHERE ${this.#scope}
|
|
326
|
+
AND g.status IN ('failed','ambiguous') ORDER BY stage,id`)
|
|
327
|
+
.all(TRAINING_RECIPE_VERSION, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION),
|
|
328
|
+
queryStages: this.#db.prepare(`SELECT stage,status,COUNT(*) count FROM training_steps
|
|
329
|
+
WHERE json_extract(request_json,'$.recipe')=? GROUP BY stage,status ORDER BY stage,status`).all(TRAINING_RECIPE_VERSION),
|
|
278
330
|
queryAttempts: this.#db.prepare(`SELECT s.stage,a.status,COUNT(*) count,
|
|
279
331
|
COUNT(json_extract(a.result_json,'$.usage.input_tokens')) usageReported,
|
|
280
332
|
SUM(json_extract(a.result_json,'$.usage.input_tokens')) inputTokens,
|
|
281
333
|
SUM(json_extract(a.result_json,'$.usage.output_tokens')) outputTokens
|
|
282
|
-
FROM training_step_attempts a JOIN training_steps s ON s.id=a.step_id
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
COALESCE(SUM(json_extract(result_json,'$.usage.
|
|
286
|
-
|
|
334
|
+
FROM training_step_attempts a JOIN training_steps s ON s.id=a.step_id
|
|
335
|
+
WHERE json_extract(s.request_json,'$.recipe')=? GROUP BY s.stage,a.status`).all(TRAINING_RECIPE_VERSION),
|
|
336
|
+
attempts: this.#db.prepare(`SELECT a.status,COUNT(*) count,
|
|
337
|
+
COALESCE(SUM(json_extract(a.result_json,'$.usage.input_tokens')),0) inputTokens,
|
|
338
|
+
COALESCE(SUM(json_extract(a.result_json,'$.usage.output_tokens')),0) outputTokens
|
|
339
|
+
FROM training_attempts a JOIN training_gates g ON g.id=a.gate_id WHERE ${this.#scope} GROUP BY a.status`)
|
|
340
|
+
.all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION) };
|
|
287
341
|
}
|
|
288
342
|
*exportRows(threshold) {
|
|
289
343
|
const rows = this.#db.prepare(`SELECT g.*,i.input_json FROM training_gates g JOIN training_inputs i ON i.hash=g.input_hash
|
|
290
|
-
WHERE ${this.#scope} AND g.status='complete' ORDER BY g.id`).iterate(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
|
|
344
|
+
WHERE ${this.#scope} AND g.status='complete' ORDER BY g.id`).iterate(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
|
|
291
345
|
for (const row of rows) {
|
|
292
346
|
yield { stage: "recall-gate", inputHash: row.input_hash, preparation: TRAINING_PREPARATION,
|
|
293
347
|
input: JSON.parse(String(row.input_json)), recallProbability: row.probability,
|
package/dist/src/training.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export declare function collectTraining(source: Source, store?: TrainingStore, o
|
|
|
22
22
|
changed: number;
|
|
23
23
|
unchanged: number;
|
|
24
24
|
retired: number;
|
|
25
|
+
review: {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
seq: number;
|
|
28
|
+
reason: string;
|
|
29
|
+
}[];
|
|
25
30
|
};
|
|
26
31
|
/** Parallel, bounded paid work. Negative results are just as cacheable as positives. */
|
|
27
32
|
export declare function runTraining(source: Source, store: TrainingStore, config: UnblockMemoryConfig, options: {
|
|
@@ -43,6 +48,11 @@ export declare function runTraining(source: Source, store: TrainingStore, config
|
|
|
43
48
|
changed: number;
|
|
44
49
|
unchanged: number;
|
|
45
50
|
retired: number;
|
|
51
|
+
review: {
|
|
52
|
+
sessionId: string;
|
|
53
|
+
seq: number;
|
|
54
|
+
reason: string;
|
|
55
|
+
}[];
|
|
46
56
|
};
|
|
47
57
|
pendingSelected: number;
|
|
48
58
|
calls: number;
|
package/dist/src/training.js
CHANGED
|
@@ -6,12 +6,13 @@ export function collectTraining(source, store, options = {}) {
|
|
|
6
6
|
const since = options.since ?? 0, until = options.until ?? Number.MAX_SAFE_INTEGER;
|
|
7
7
|
const reader = new TrainingTranscriptReader(source.databasePath, source.agentId);
|
|
8
8
|
const result = { sessions: 0, excludedSessions: 0, oversizedSessions: 0, eligible: 0,
|
|
9
|
-
users: 0, filtered: 0, oversized: 0, unanswered: 0, added: 0, changed: 0, unchanged: 0, retired: 0
|
|
9
|
+
users: 0, filtered: 0, oversized: 0, unanswered: 0, added: 0, changed: 0, unchanged: 0, retired: 0,
|
|
10
|
+
review: [] };
|
|
10
11
|
try {
|
|
11
12
|
const sessions = new Set(options.existingOnly ? store?.sessions() : [...reader.sessions(), ...store?.sessions() ?? []]);
|
|
12
13
|
for (const id of sessions) {
|
|
13
14
|
store?.renew();
|
|
14
|
-
const extracted = reader.read(id);
|
|
15
|
+
const extracted = reader.read(id, () => store?.renew());
|
|
15
16
|
result.sessions++;
|
|
16
17
|
if (extracted && "oversized" in extracted) {
|
|
17
18
|
result.oversizedSessions++;
|
|
@@ -21,6 +22,7 @@ export function collectTraining(source, store, options = {}) {
|
|
|
21
22
|
if (!extracted)
|
|
22
23
|
result.excludedSessions++;
|
|
23
24
|
else {
|
|
25
|
+
result.review.push(...extracted.review.map(item => ({ sessionId: id, ...item })));
|
|
24
26
|
for (const key of ["users", "filtered", "oversized", "unanswered"])
|
|
25
27
|
result[key] += extracted.coverage[key];
|
|
26
28
|
result.eligible += extracted.examples.filter(e => e.timestamp >= since && e.timestamp < until).length;
|
|
@@ -54,7 +56,7 @@ export async function runTraining(source, store, config, options) {
|
|
|
54
56
|
throw new Error("TypeSafe is disabled or its credential is unavailable");
|
|
55
57
|
let next = 0, stopped = false;
|
|
56
58
|
const workers = await Promise.allSettled(Array.from({ length: Math.min(concurrency, jobs.length) }, async () => {
|
|
57
|
-
while (!stopped && !result.budgetLimited
|
|
59
|
+
while (!stopped && !result.budgetLimited) {
|
|
58
60
|
const job = jobs[next++];
|
|
59
61
|
if (!job)
|
|
60
62
|
return;
|
|
@@ -83,8 +85,8 @@ export async function runTraining(source, store, config, options) {
|
|
|
83
85
|
store.finish(job.id, attempt, { status, error: error instanceof TypeSafeRequestError && error.code === "http_error" ?
|
|
84
86
|
`http_${error.status}` : "request_or_response_uncertain" });
|
|
85
87
|
result[status]++;
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
+
// Flag this input for operator review; do not stall untouched inputs or retry it.
|
|
89
|
+
continue;
|
|
88
90
|
}
|
|
89
91
|
// Storage failures must not be misclassified as provider failures/retried.
|
|
90
92
|
store.finish(job.id, attempt, judgment);
|
package/dist/src/typesafe.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { QueryConversation } from "./query-contract.js";
|
|
2
2
|
/** Select from trusted candidates; never accept a provider-generated path or skill name. */
|
|
3
3
|
export declare function selectTypeSafeSkill(params: {
|
|
4
4
|
apiKey: string;
|
|
@@ -29,15 +29,65 @@ export declare function judgeTypeSafeQuality(params: {
|
|
|
29
29
|
sourceKind: "files" | "sessions";
|
|
30
30
|
}[];
|
|
31
31
|
}): Promise<QualityJudgment[]>;
|
|
32
|
-
|
|
32
|
+
export declare const MEMORY_JUDGE_VERSION = "jev-1.13.0:memory-usefulness-v2";
|
|
33
|
+
export type MemoryPassage = {
|
|
34
|
+
excerpt: string;
|
|
35
|
+
corpus: string;
|
|
36
|
+
sourcePath: string;
|
|
37
|
+
dates: readonly string[];
|
|
38
|
+
};
|
|
39
|
+
/** The sole versioned passage prompt/state builder for both training and runtime. */
|
|
40
|
+
export declare function memoryUsefulnessRequest(conversation: QueryConversation, candidate: MemoryPassage, asOf: string): {
|
|
41
|
+
model: string;
|
|
42
|
+
state: {
|
|
43
|
+
conversation: {
|
|
44
|
+
history: {
|
|
45
|
+
role: "user" | "assistant";
|
|
46
|
+
content: string;
|
|
47
|
+
}[];
|
|
48
|
+
currentRequest: string;
|
|
49
|
+
};
|
|
50
|
+
asOf: string;
|
|
51
|
+
candidates: {
|
|
52
|
+
excerpt: string;
|
|
53
|
+
corpus: string;
|
|
54
|
+
sourcePath: string;
|
|
55
|
+
dates: readonly string[];
|
|
56
|
+
}[];
|
|
57
|
+
};
|
|
58
|
+
questions: {
|
|
59
|
+
memory_0: {
|
|
60
|
+
type: string;
|
|
61
|
+
instructions: string;
|
|
62
|
+
criteria: {
|
|
63
|
+
true: string;
|
|
64
|
+
false: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
export declare function judgeMemoryPassage(request: ReturnType<typeof memoryUsefulnessRequest>, params: {
|
|
70
|
+
apiKey: string;
|
|
71
|
+
timeoutMs: number;
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
}): Promise<{
|
|
74
|
+
probability: number;
|
|
75
|
+
answer: {
|
|
76
|
+
type: "noul";
|
|
77
|
+
noul: number;
|
|
78
|
+
};
|
|
79
|
+
model: "jev-1.13.0";
|
|
80
|
+
usage: {
|
|
81
|
+
input_tokens: number;
|
|
82
|
+
output_tokens: number;
|
|
83
|
+
} | null;
|
|
84
|
+
}>;
|
|
85
|
+
/** One HTTP request per candidate; result order matches input order. */
|
|
33
86
|
export declare function judgeTypeSafeMemories(params: {
|
|
34
87
|
apiKey: string;
|
|
35
88
|
timeoutMs: number;
|
|
36
89
|
signal: AbortSignal;
|
|
37
|
-
conversation:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
corpus: string;
|
|
41
|
-
messageTimestamp?: string;
|
|
42
|
-
}[];
|
|
90
|
+
conversation: QueryConversation;
|
|
91
|
+
asOf: string;
|
|
92
|
+
candidates: readonly MemoryPassage[];
|
|
43
93
|
}): Promise<number[]>;
|