claude-flow 3.18.2 → 3.20.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/package.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/neural.js +109 -14
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-tools.js +182 -2
- package/v3/@claude-flow/cli/dist/src/ruvector/index.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/index.js +1 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/lora-adapter.d.ts +35 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/lora-adapter.js +108 -1
- package/v3/@claude-flow/cli/dist/src/services/native-training.d.ts +68 -0
- package/v3/@claude-flow/cli/dist/src/services/native-training.js +141 -0
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"@grpc/grpc-js": ">=1.14.4",
|
|
118
118
|
"form-data": ">=4.0.6",
|
|
119
119
|
"http-proxy-middleware": ">=3.0.7",
|
|
120
|
-
"@ruvector/ruvllm": ">=2.
|
|
120
|
+
"@ruvector/ruvllm": ">=2.6.0"
|
|
121
121
|
},
|
|
122
122
|
"devDependencies": {
|
|
123
123
|
"@openai/codex": "^0.98.0",
|
|
@@ -23,6 +23,9 @@ const trainCommand = {
|
|
|
23
23
|
{ name: 'hyperbolic', type: 'boolean', description: 'Enable hyperbolic attention for hierarchical patterns', default: 'false' },
|
|
24
24
|
{ name: 'contrastive', type: 'boolean', description: 'Use contrastive learning (InfoNCE)', default: 'true' },
|
|
25
25
|
{ name: 'curriculum', type: 'boolean', description: 'Enable curriculum learning', default: 'false' },
|
|
26
|
+
{ name: 'backend', type: 'string', description: 'Training backend: auto (native when available), native (@ruvector/ruvllm TrainingPipeline, disk checkpoints), wasm (RuVector MicroLoRA/InfoNCE)', default: 'auto' },
|
|
27
|
+
{ name: 'val-split', type: 'number', description: 'Validation holdout fraction 0..1 (native backend). >0 reports Best Val Loss + early stopping; 0 disables', default: '0.1' },
|
|
28
|
+
{ name: 'resume', type: 'string', description: 'Resume native training from a checkpoint path (weights on 2.5.7; epoch position on >=2.6.0). Native backend only', default: '' },
|
|
26
29
|
],
|
|
27
30
|
examples: [
|
|
28
31
|
{ command: 'claude-flow neural train -p coordination -e 100', description: 'Train coordination patterns' },
|
|
@@ -35,6 +38,22 @@ const trainCommand = {
|
|
|
35
38
|
const learningRate = parseFloat(ctx.flags['learning-rate'] || '0.01');
|
|
36
39
|
const batchSize = parseInt(ctx.flags['batch-size'] || '32', 10);
|
|
37
40
|
const dim = Math.min(parseInt(ctx.flags.dim || '256', 10), 256);
|
|
41
|
+
// #2549 follow-up — backend routing: 'native' = @ruvector/ruvllm
|
|
42
|
+
// TrainingPipeline (real epochs/early-stopping/disk checkpoints),
|
|
43
|
+
// 'wasm' = RuVector MicroLoRA/InfoNCE (pre-3.19 behavior),
|
|
44
|
+
// 'auto' = native when the module resolves, else wasm.
|
|
45
|
+
const backendFlag = String(ctx.flags.backend || 'auto');
|
|
46
|
+
// Feature: validation split + resume (native TrainingPipeline leg).
|
|
47
|
+
const valSplitRaw = parseFloat(ctx.flags['val-split'] ?? '0.1');
|
|
48
|
+
const valSplit = Number.isFinite(valSplitRaw) ? Math.max(0, Math.min(1, valSplitRaw)) : 0.1;
|
|
49
|
+
const resumePath = ctx.flags.resume ? String(ctx.flags.resume) : undefined;
|
|
50
|
+
// --resume is a native-only capability; refuse the WASM combination up
|
|
51
|
+
// front so the user gets a clear error rather than a silently-ignored flag.
|
|
52
|
+
if (resumePath && backendFlag === 'wasm') {
|
|
53
|
+
output.writeln();
|
|
54
|
+
output.writeln(output.error('--resume is only supported by the native backend; drop --backend wasm.'));
|
|
55
|
+
return { success: false, exitCode: 1 };
|
|
56
|
+
}
|
|
38
57
|
const useWasm = ctx.flags.wasm !== false;
|
|
39
58
|
const useFlash = ctx.flags.flash !== false;
|
|
40
59
|
const useMoE = ctx.flags.moe === true;
|
|
@@ -181,6 +200,50 @@ const trainCommand = {
|
|
|
181
200
|
}
|
|
182
201
|
}
|
|
183
202
|
spinner.setText(`Training with ${embeddings.length} embeddings...`);
|
|
203
|
+
// #2549 — native TrainingPipeline leg. In 'auto'/'native' mode the
|
|
204
|
+
// LoRA training runs through @ruvector/ruvllm with the checkpoint
|
|
205
|
+
// taken from the TRAINED pipeline (the old best-effort block saved
|
|
206
|
+
// a fresh adapter's untrained weights). SONA/ReasoningBank
|
|
207
|
+
// persistence in the loop below runs regardless of backend.
|
|
208
|
+
const nativeTraining = await import('../services/native-training.js');
|
|
209
|
+
const useNative = backendFlag === 'native'
|
|
210
|
+
|| (backendFlag === 'auto' && nativeTraining.nativeTrainingAvailable());
|
|
211
|
+
// --resume only works on the native pipeline; if native is unavailable
|
|
212
|
+
// (module absent), fail loudly rather than silently fresh-train.
|
|
213
|
+
if (resumePath && !useNative) {
|
|
214
|
+
spinner.fail('--resume requires the native @ruvector/ruvllm backend, which is not available');
|
|
215
|
+
return { success: false, exitCode: 1 };
|
|
216
|
+
}
|
|
217
|
+
let nativeResult = null;
|
|
218
|
+
if (useNative) {
|
|
219
|
+
spinner.setText(`Training ${patternType} on native @ruvector/ruvllm pipeline...`);
|
|
220
|
+
const path = await import('path');
|
|
221
|
+
try {
|
|
222
|
+
nativeResult = await nativeTraining.runNativeTraining({
|
|
223
|
+
embeddings,
|
|
224
|
+
epochs,
|
|
225
|
+
batchSize,
|
|
226
|
+
learningRate,
|
|
227
|
+
dim,
|
|
228
|
+
validationSplit: valSplit,
|
|
229
|
+
resumeFrom: resumePath,
|
|
230
|
+
checkpointPath: path.join(process.cwd(), '.claude-flow', 'neural', `lora-checkpoint-${Date.now()}.json`),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
// ResumeFailedError — an explicit --resume that could not load is a
|
|
235
|
+
// loud, exit-1 failure, never a silent fall-through to fresh training.
|
|
236
|
+
spinner.fail(`Resume failed: ${err.message}`);
|
|
237
|
+
return { success: false, exitCode: 1 };
|
|
238
|
+
}
|
|
239
|
+
if (!nativeResult && backendFlag === 'native') {
|
|
240
|
+
spinner.fail('Native backend requested (--backend native) but @ruvector/ruvllm training failed');
|
|
241
|
+
return { success: false, exitCode: 1 };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Native handles the LoRA leg; WASM contrastive runs when native
|
|
245
|
+
// didn't (absent module, or explicit --backend wasm).
|
|
246
|
+
const runWasmLeg = !nativeResult;
|
|
184
247
|
// Main training loop with WASM acceleration
|
|
185
248
|
for (let epoch = 0; epoch < epochs; epoch++) {
|
|
186
249
|
const epochStart = performance.now();
|
|
@@ -192,7 +255,7 @@ const trainCommand = {
|
|
|
192
255
|
if (batch.length === 0)
|
|
193
256
|
continue;
|
|
194
257
|
// Training step with contrastive learning
|
|
195
|
-
if (useContrastive && batch.length >= 3 && useWasm && wasmFeatures.length > 0) {
|
|
258
|
+
if (runWasmLeg && useContrastive && batch.length >= 3 && useWasm && wasmFeatures.length > 0) {
|
|
196
259
|
const anchor = batch[0];
|
|
197
260
|
const positives = [batch[1]];
|
|
198
261
|
const negatives = batch.slice(2);
|
|
@@ -260,17 +323,22 @@ const trainCommand = {
|
|
|
260
323
|
// Flush patterns to disk
|
|
261
324
|
flushPatterns();
|
|
262
325
|
const persistence = getPersistenceStatus();
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
326
|
+
// Checkpoint: when the native pipeline trained, its checkpoint (the
|
|
327
|
+
// TRAINED weights) was already written by runNativeTraining. The
|
|
328
|
+
// pre-3.19 fallback below saved a FRESH adapter's weights — only
|
|
329
|
+
// meaningful as a fallback when the native leg didn't run.
|
|
330
|
+
if (!nativeResult?.checkpointPath) {
|
|
331
|
+
try {
|
|
332
|
+
const { LoRAAdapter } = await import('../ruvector/lora-adapter.js');
|
|
333
|
+
const path = await import('path');
|
|
334
|
+
const cpDir = path.join(process.cwd(), '.claude-flow', 'neural');
|
|
335
|
+
const cpPath = path.join(cpDir, `lora-checkpoint-${Date.now()}.json`);
|
|
336
|
+
const adapter = new LoRAAdapter({ inputDim: dim, outputDim: dim, rank: 4 });
|
|
337
|
+
await adapter.initBackend();
|
|
338
|
+
await adapter.saveCheckpoint(cpPath);
|
|
339
|
+
}
|
|
340
|
+
catch { /* checkpoint save is best-effort */ }
|
|
272
341
|
}
|
|
273
|
-
catch { /* checkpoint save is best-effort */ }
|
|
274
342
|
output.writeln();
|
|
275
343
|
// Display results
|
|
276
344
|
const tableData = [
|
|
@@ -284,8 +352,31 @@ const trainCommand = {
|
|
|
284
352
|
{ metric: 'Total Time', value: `${(totalTime / 1000).toFixed(1)}s` },
|
|
285
353
|
{ metric: 'Avg Epoch Time', value: `${(epochTimes.reduce((a, b) => a + b, 0) / epochTimes.length).toFixed(2)}ms` },
|
|
286
354
|
];
|
|
355
|
+
// Native pipeline metrics (#2549 — the LoRA leg trained on ruvllm)
|
|
356
|
+
if (nativeResult) {
|
|
357
|
+
tableData.push({ metric: 'Backend', value: 'native (@ruvector/ruvllm TrainingPipeline)' }, { metric: 'Native Steps', value: String(nativeResult.steps) }, { metric: 'Final Loss', value: nativeResult.finalLoss.toExponential(3) });
|
|
358
|
+
// Validation metrics only surface when a holdout actually ran
|
|
359
|
+
// (bestValLoss is non-null); Early Stopped is only meaningful then.
|
|
360
|
+
if (nativeResult.bestValLoss !== null && nativeResult.bestValLoss !== undefined) {
|
|
361
|
+
tableData.push({ metric: 'Best Val Loss', value: nativeResult.bestValLoss.toExponential(3) }, { metric: 'Early Stopped', value: nativeResult.earlyStopped ? 'yes' : 'no' });
|
|
362
|
+
}
|
|
363
|
+
if (nativeResult.resumed) {
|
|
364
|
+
tableData.push({
|
|
365
|
+
metric: 'Resumed',
|
|
366
|
+
value: nativeResult.resumeMode === 'resumeFrom'
|
|
367
|
+
? `${resumePath} (epoch position restored)`
|
|
368
|
+
: `${resumePath} (weights only — epoch-position resume needs @ruvector/ruvllm >=2.6.0)`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (nativeResult.checkpointPath) {
|
|
372
|
+
tableData.push({
|
|
373
|
+
metric: 'Checkpoint',
|
|
374
|
+
value: `${nativeResult.checkpointPath}${nativeResult.checkpointBytes ? ` (${(nativeResult.checkpointBytes / 1024).toFixed(1)} KB)` : ''}`,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
287
378
|
// Add WASM-specific metrics
|
|
288
|
-
if (useWasm && wasmFeatures.length > 0) {
|
|
379
|
+
if (runWasmLeg && useWasm && wasmFeatures.length > 0) {
|
|
289
380
|
const backendUsed = ruvectorStats?.backend || 'unknown';
|
|
290
381
|
tableData.push({ metric: 'Backend', value: backendUsed === 'wasm' ? 'WASM (native)' : 'JS (fallback)' }, { metric: 'WASM Features', value: wasmFeatures.slice(0, 3).join(', ') }, { metric: 'LoRA Adaptations', value: String(adaptations) }, { metric: 'Avg Loss', value: (totalLoss / Math.max(1, epochs)).toFixed(4) });
|
|
291
382
|
if (ruvectorStats?.microLoraStats) {
|
|
@@ -474,10 +565,14 @@ const statusCommand = {
|
|
|
474
565
|
details: stats._trainingBackend === 'ruvllm'
|
|
475
566
|
? await (async () => {
|
|
476
567
|
try {
|
|
477
|
-
const { nativeCheckpointsSupported } = await import('../ruvector/lora-adapter.js');
|
|
478
|
-
|
|
568
|
+
const { nativeCheckpointsSupported, latestCheckpointInfo } = await import('../ruvector/lora-adapter.js');
|
|
569
|
+
// Most important info first (truncation-friendly): backend
|
|
570
|
+
// capability, then the newest checkpoint + age when one exists.
|
|
571
|
+
const base = nativeCheckpointsSupported()
|
|
479
572
|
? 'native @ruvector/ruvllm pipeline + disk checkpoints'
|
|
480
573
|
: 'native @ruvector/ruvllm pipeline (checkpoints need >=2.5.7)';
|
|
574
|
+
const cp = latestCheckpointInfo();
|
|
575
|
+
return cp ? `${base} · latest: ${cp.filename} (${cp.ageLabel})` : base;
|
|
481
576
|
}
|
|
482
577
|
catch {
|
|
483
578
|
return 'native @ruvector/ruvllm pipeline';
|
|
@@ -121,6 +121,7 @@ export const agenticowTools = [
|
|
|
121
121
|
branchPath: { type: 'string', description: 'Path to write the branch file' },
|
|
122
122
|
label: { type: 'string', description: 'Human-readable label for the branch (alnum + _.-:/@ only)' },
|
|
123
123
|
dimension: { type: 'integer', description: 'Vector dimension (required only when basePath does not exist yet)' },
|
|
124
|
+
nativeAnn: { type: 'boolean', description: 'Use the native Rust COW dual-graph ANN path (recall@10=1.0, query spans the COW boundary in one Rust call). Default false (exact JS chain-walk). Set true when the branch will be queried.', default: false },
|
|
124
125
|
},
|
|
125
126
|
required: ['basePath', 'branchPath', 'label'],
|
|
126
127
|
},
|
|
@@ -132,9 +133,10 @@ export const agenticowTools = [
|
|
|
132
133
|
const basePath = resolveMemoryPath(String(input.basePath));
|
|
133
134
|
const branchPath = resolveMemoryPath(String(input.branchPath));
|
|
134
135
|
const dim = input.dimension;
|
|
136
|
+
const nativeAnn = input.nativeAnn === true;
|
|
135
137
|
const base = await openWithLineage(api, basePath, dim);
|
|
136
138
|
try {
|
|
137
|
-
const branch = await base.fork(label, branchPath);
|
|
139
|
+
const branch = await base.fork(label, branchPath, { nativeAnn });
|
|
138
140
|
// Persist lineage manifests so the branch (and base) reopen with
|
|
139
141
|
// their COW chain intact. Without this, fork is in-memory only.
|
|
140
142
|
await branch.save?.(manifestFor(branchPath));
|
|
@@ -145,6 +147,7 @@ export const agenticowTools = [
|
|
|
145
147
|
basePath,
|
|
146
148
|
branchPath,
|
|
147
149
|
label,
|
|
150
|
+
nativeAnn,
|
|
148
151
|
};
|
|
149
152
|
}
|
|
150
153
|
finally {
|
|
@@ -152,6 +155,181 @@ export const agenticowTools = [
|
|
|
152
155
|
}
|
|
153
156
|
},
|
|
154
157
|
},
|
|
158
|
+
{
|
|
159
|
+
name: 'agenticow_ingest',
|
|
160
|
+
description: 'agenticow — write vectors (with optional text payloads) into an .rvf memory branch or base. Records: [{id?, vector, text?}] — id auto-assigns when omitted. This is the write half that makes a branch usable: agenticow_branch creates an empty COW child, but without ingest it has nothing to read back. Use after branching to populate an agent/session-local memory. Editing the base directly is wrong when the writes are speculative — ingest into a branch, then promote only if validated. Persists via .agenticow.json lineage manifest.',
|
|
161
|
+
category: 'memory',
|
|
162
|
+
tags: ['agenticow', 'memory', 'cow', 'ingest', 'write'],
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
path: { type: 'string', description: 'Path to .rvf memory file (branch or base)' },
|
|
167
|
+
records: {
|
|
168
|
+
type: 'array',
|
|
169
|
+
description: 'Vectors to ingest: [{id?: number, vector: number[], text?: string}]',
|
|
170
|
+
items: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
id: { type: 'integer', description: 'Explicit id (auto-assigned when omitted)' },
|
|
174
|
+
vector: { type: 'array', items: { type: 'number' }, description: 'Embedding vector (length must equal the memory dimension)' },
|
|
175
|
+
text: { type: 'string', description: 'Optional payload surfaced on query hits' },
|
|
176
|
+
},
|
|
177
|
+
required: ['vector'],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
dimension: { type: 'integer', description: 'Vector dimension (required only when path does not exist yet)' },
|
|
181
|
+
},
|
|
182
|
+
required: ['path', 'records'],
|
|
183
|
+
},
|
|
184
|
+
handler: async (input) => {
|
|
185
|
+
const api = await loadAgenticow();
|
|
186
|
+
if (!api)
|
|
187
|
+
return degradedResult('agenticow-not-found');
|
|
188
|
+
const path = resolveMemoryPath(String(input.path));
|
|
189
|
+
const records = input.records;
|
|
190
|
+
if (!Array.isArray(records) || records.length === 0) {
|
|
191
|
+
throw new Error('records must be a non-empty array of {id?, vector, text?}');
|
|
192
|
+
}
|
|
193
|
+
for (const r of records) {
|
|
194
|
+
if (!Array.isArray(r.vector) || r.vector.length === 0) {
|
|
195
|
+
throw new Error('each record requires a non-empty numeric vector');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const dim = input.dimension ?? records[0].vector.length;
|
|
199
|
+
const mem = await openWithLineage(api, path, dim);
|
|
200
|
+
try {
|
|
201
|
+
const result = await mem.ingest(records.map((r) => ({
|
|
202
|
+
...(typeof r.id === 'number' ? { id: r.id } : {}),
|
|
203
|
+
vector: r.vector,
|
|
204
|
+
...(r.text !== undefined ? { text: r.text } : {}),
|
|
205
|
+
})));
|
|
206
|
+
await mem.save?.(manifestFor(path));
|
|
207
|
+
return { success: true, path, ingested: result };
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
await mem.close?.();
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'agenticow_query',
|
|
216
|
+
description: 'agenticow — k-NN read across an .rvf memory branch\'s full COW lineage (parent ∪ edits, child wins), returning {id, distance, branch, text}. Read-only (no manifest write). The `branch` field on each hit tells you which lineage node the result came from — the read-through semantics that make branching useful. Use to retrieve from an agent/session branch without materializing a full copy. Re-opening the base and manually merging edits is wrong: query already spans the chain (and uses the single-call Rust path when the branch was forked with nativeAnn).',
|
|
217
|
+
category: 'memory',
|
|
218
|
+
tags: ['agenticow', 'memory', 'cow', 'query', 'read', 'search'],
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {
|
|
222
|
+
path: { type: 'string', description: 'Path to .rvf memory file' },
|
|
223
|
+
vector: { type: 'array', items: { type: 'number' }, description: 'Query embedding vector' },
|
|
224
|
+
k: { type: 'integer', description: 'Number of nearest neighbors to return', default: 10 },
|
|
225
|
+
efSearch: { type: 'integer', description: 'HNSW efSearch per lineage store (higher = better recall, slower)' },
|
|
226
|
+
},
|
|
227
|
+
required: ['path', 'vector'],
|
|
228
|
+
},
|
|
229
|
+
handler: async (input) => {
|
|
230
|
+
const api = await loadAgenticow();
|
|
231
|
+
if (!api)
|
|
232
|
+
return degradedResult('agenticow-not-found');
|
|
233
|
+
const path = resolveMemoryPath(String(input.path));
|
|
234
|
+
const vector = input.vector;
|
|
235
|
+
if (!Array.isArray(vector) || vector.length === 0) {
|
|
236
|
+
throw new Error('vector must be a non-empty numeric array');
|
|
237
|
+
}
|
|
238
|
+
const k = typeof input.k === 'number' ? input.k : 10;
|
|
239
|
+
const opts = {};
|
|
240
|
+
if (typeof input.efSearch === 'number')
|
|
241
|
+
opts.efSearch = input.efSearch;
|
|
242
|
+
const mem = await openWithLineage(api, path);
|
|
243
|
+
try {
|
|
244
|
+
const hits = await mem.query(vector, k, opts);
|
|
245
|
+
return { success: true, path, k, hits };
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
await mem.close?.();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'agenticow_diff',
|
|
254
|
+
description: 'agenticow — show what a branch changed relative to its lineage: {added, overridden, deleted} vector-id lists. Use before promote to preview the exact merge, or to audit what an agent/session branch actually wrote. Diffing by re-querying is wrong because deletions (tombstones) are invisible to a read — diff() surfaces them explicitly. Requires the branch was opened with edit tracking (default on).',
|
|
255
|
+
category: 'memory',
|
|
256
|
+
tags: ['agenticow', 'memory', 'cow', 'diff'],
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties: {
|
|
260
|
+
path: { type: 'string', description: 'Path to branch .rvf file' },
|
|
261
|
+
},
|
|
262
|
+
required: ['path'],
|
|
263
|
+
},
|
|
264
|
+
handler: async (input) => {
|
|
265
|
+
const api = await loadAgenticow();
|
|
266
|
+
if (!api)
|
|
267
|
+
return degradedResult('agenticow-not-found');
|
|
268
|
+
const path = resolveMemoryPath(String(input.path));
|
|
269
|
+
const mem = await openWithLineage(api, path);
|
|
270
|
+
try {
|
|
271
|
+
const diff = await mem.diff();
|
|
272
|
+
return { success: true, path, diff };
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
await mem.close?.();
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'agenticow_lineage',
|
|
281
|
+
description: 'agenticow — walk the COW chain of an .rvf memory file: an ordered list of nodes (role working|checkpoint|base, id, label, parent, createdAt, mutations, tombstones). Use to understand branch history, find checkpoint ids for a targeted rollback, or debug a promote. Guessing the chain from filenames is wrong — lineage is the authoritative structure the store maintains.',
|
|
282
|
+
category: 'memory',
|
|
283
|
+
tags: ['agenticow', 'memory', 'cow', 'lineage', 'history'],
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
path: { type: 'string', description: 'Path to .rvf memory file' },
|
|
288
|
+
},
|
|
289
|
+
required: ['path'],
|
|
290
|
+
},
|
|
291
|
+
handler: async (input) => {
|
|
292
|
+
const api = await loadAgenticow();
|
|
293
|
+
if (!api)
|
|
294
|
+
return degradedResult('agenticow-not-found');
|
|
295
|
+
const path = resolveMemoryPath(String(input.path));
|
|
296
|
+
const mem = await openWithLineage(api, path);
|
|
297
|
+
try {
|
|
298
|
+
const lineage = await mem.lineage();
|
|
299
|
+
return { success: true, path, lineage };
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
await mem.close?.();
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'agenticow_status',
|
|
308
|
+
description: 'agenticow — health/geometry of an .rvf memory file: {totalVectors, totalSegments, fileSize, currentEpoch, deadSpaceRatio, readOnly, chainDepth, dimension, metric}. Use to check vector count before/after ingest, spot compaction pressure (deadSpaceRatio), or confirm the dimension before ingesting into a shared base. Pure read.',
|
|
309
|
+
category: 'memory',
|
|
310
|
+
tags: ['agenticow', 'memory', 'cow', 'status'],
|
|
311
|
+
inputSchema: {
|
|
312
|
+
type: 'object',
|
|
313
|
+
properties: {
|
|
314
|
+
path: { type: 'string', description: 'Path to .rvf memory file' },
|
|
315
|
+
},
|
|
316
|
+
required: ['path'],
|
|
317
|
+
},
|
|
318
|
+
handler: async (input) => {
|
|
319
|
+
const api = await loadAgenticow();
|
|
320
|
+
if (!api)
|
|
321
|
+
return degradedResult('agenticow-not-found');
|
|
322
|
+
const path = resolveMemoryPath(String(input.path));
|
|
323
|
+
const mem = await openWithLineage(api, path);
|
|
324
|
+
try {
|
|
325
|
+
const status = await mem.status();
|
|
326
|
+
return { success: true, path, status };
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
await mem.close?.();
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
},
|
|
155
333
|
{
|
|
156
334
|
name: 'agenticow_checkpoint',
|
|
157
335
|
description: 'agenticow — freeze a labelled restore point on an .rvf memory file. Subsequent edits stay in a fresh COW child; rollback returns here. Use when you are about to run an experimental Darwin tick or speculative agent edit that may need to be discarded. Relying on the working node alone is wrong because there is no "undo last N writes" semantics — without a checkpoint, a bad ingest contaminates the base. Persists via .agenticow.json lineage manifest so it survives close+reopen.',
|
|
@@ -191,6 +369,7 @@ export const agenticowTools = [
|
|
|
191
369
|
type: 'object',
|
|
192
370
|
properties: {
|
|
193
371
|
path: { type: 'string', description: 'Path to .rvf memory file' },
|
|
372
|
+
checkpointId: { type: 'string', description: 'Target checkpoint id from agenticow_lineage (omit to roll back to the most recent checkpoint)' },
|
|
194
373
|
},
|
|
195
374
|
required: ['path'],
|
|
196
375
|
},
|
|
@@ -199,9 +378,10 @@ export const agenticowTools = [
|
|
|
199
378
|
if (!api)
|
|
200
379
|
return degradedResult('agenticow-not-found');
|
|
201
380
|
const path = resolveMemoryPath(String(input.path));
|
|
381
|
+
const checkpointId = input.checkpointId ? String(input.checkpointId) : undefined;
|
|
202
382
|
const mem = await openWithLineage(api, path);
|
|
203
383
|
try {
|
|
204
|
-
const r = await mem.rollback();
|
|
384
|
+
const r = checkpointId ? await mem.rollback(checkpointId) : await mem.rollback();
|
|
205
385
|
await mem.save?.(manifestFor(path));
|
|
206
386
|
return { success: true, path, rolledBack: true, result: r };
|
|
207
387
|
}
|
|
@@ -18,7 +18,7 @@ export { DiffClassifier, createDiffClassifier, analyzeDiff, analyzeDiffSync, ass
|
|
|
18
18
|
export { CoverageRouter, createCoverageRouter, coverageRoute, coverageSuggest, coverageGaps, clearCoverageCache, getCoverageCacheStats, type CoverageRouterConfig, type FileCoverage, type CoverageReport, type CoverageRouteResult, type CoverageSuggestResult, type CoverageGapsResult, type CoverageRouteOptions, type CoverageSuggestOptions, type CoverageGapsOptions, } from './coverage-router.js';
|
|
19
19
|
export { coverageRouterTools, hooksCoverageRoute, hooksCoverageSuggest, hooksCoverageGaps } from './coverage-tools.js';
|
|
20
20
|
export { buildDependencyGraph, analyzeGraph, analyzeMinCutBoundaries, analyzeModuleCommunities, detectCircularDependencies, exportToDot, loadRuVector, fallbackMinCut, fallbackLouvain, clearGraphCaches, getGraphCacheStats, type GraphNode, type GraphEdge, type DependencyGraph, type MinCutBoundary, type ModuleCommunity, type CircularDependency, type GraphAnalysisResult, } from './graph-analyzer.js';
|
|
21
|
-
export { LoRAAdapter, getLoRAAdapter, resetLoRAAdapter, createLoRAAdapter, adaptEmbedding, trainLoRA, getLoRAStats, DEFAULT_RANK, DEFAULT_ALPHA, INPUT_DIM as LORA_INPUT_DIM, OUTPUT_DIM as LORA_OUTPUT_DIM, type LoRAConfig, type LoRAWeights, type AdaptationResult, type LoRAStats, } from './lora-adapter.js';
|
|
21
|
+
export { LoRAAdapter, getLoRAAdapter, resetLoRAAdapter, createLoRAAdapter, adaptEmbedding, trainLoRA, getLoRAStats, loadLatestCheckpoint, latestCheckpointInfo, formatCheckpointAge, DEFAULT_RANK, DEFAULT_ALPHA, INPUT_DIM as LORA_INPUT_DIM, OUTPUT_DIM as LORA_OUTPUT_DIM, type LoRAConfig, type LoRAWeights, type AdaptationResult, type LoRAStats, type CheckpointInfo, } from './lora-adapter.js';
|
|
22
22
|
export { ModelRouter, getModelRouter, resetModelRouter, createModelRouter, routeToModel, routeToModelFull, analyzeTaskComplexity, getModelRouterStats, recordModelOutcome, MODEL_CAPABILITIES, COMPLEXITY_INDICATORS, type ClaudeModel, type ModelRouterConfig, type ModelRoutingResult, type ComplexityAnalysis, } from './model-router.js';
|
|
23
23
|
export { SemanticRouter, createSemanticRouter, type Intent, type RouteResult, type RouterConfig, } from './semantic-router.js';
|
|
24
24
|
export { isRuvllmWasmAvailable, initRuvllmWasm, getRuvllmStatus, createHnswRouter, createSonaInstant, createMicroLora, formatChat, createKvCache, createGenerateConfig, createBufferPool, createInferenceArena, HNSW_MAX_SAFE_PATTERNS, type HnswRouterConfig, type HnswPattern, type HnswRouteResult, type SonaConfig, type MicroLoraConfig, type ChatMessage, type GenerateOptions, type RuvllmStatus, } from './ruvllm-wasm.js';
|
|
@@ -36,7 +36,7 @@ clearGraphCaches, getGraphCacheStats, } from './graph-analyzer.js';
|
|
|
36
36
|
// consumers (hooks-tools.ts, neural-tools.ts) import from '@claude-flow/neural'
|
|
37
37
|
// explicitly; re-exporting through this barrel pulls the package's
|
|
38
38
|
// transitive @ruvector/sona dep into vitest's eager resolution.
|
|
39
|
-
export { LoRAAdapter, getLoRAAdapter, resetLoRAAdapter, createLoRAAdapter, adaptEmbedding, trainLoRA, getLoRAStats, DEFAULT_RANK, DEFAULT_ALPHA, INPUT_DIM as LORA_INPUT_DIM, OUTPUT_DIM as LORA_OUTPUT_DIM, } from './lora-adapter.js';
|
|
39
|
+
export { LoRAAdapter, getLoRAAdapter, resetLoRAAdapter, createLoRAAdapter, adaptEmbedding, trainLoRA, getLoRAStats, loadLatestCheckpoint, latestCheckpointInfo, formatCheckpointAge, DEFAULT_RANK, DEFAULT_ALPHA, INPUT_DIM as LORA_INPUT_DIM, OUTPUT_DIM as LORA_OUTPUT_DIM, } from './lora-adapter.js';
|
|
40
40
|
export { ModelRouter, getModelRouter, resetModelRouter, createModelRouter, routeToModel, routeToModelFull, analyzeTaskComplexity, getModelRouterStats, recordModelOutcome, MODEL_CAPABILITIES, COMPLEXITY_INDICATORS, } from './model-router.js';
|
|
41
41
|
export { SemanticRouter, createSemanticRouter, } from './semantic-router.js';
|
|
42
42
|
// ── RuVector LLM WASM (inference utilities) ─────────────────
|
|
@@ -213,6 +213,41 @@ export declare class LoRAAdapter {
|
|
|
213
213
|
scaling: number;
|
|
214
214
|
}): boolean;
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Info about the newest on-disk training checkpoint.
|
|
218
|
+
*/
|
|
219
|
+
export interface CheckpointInfo {
|
|
220
|
+
/** Absolute path to the checkpoint file */
|
|
221
|
+
path: string;
|
|
222
|
+
/** Basename, e.g. lora-checkpoint-1712345678901.json */
|
|
223
|
+
filename: string;
|
|
224
|
+
/** Age in ms derived from the filename timestamp (falls back to mtime) */
|
|
225
|
+
ageMs: number;
|
|
226
|
+
/** Human-friendly age, e.g. "2h ago" */
|
|
227
|
+
ageLabel: string;
|
|
228
|
+
}
|
|
229
|
+
/** Compact relative-age label. Most-significant unit only. */
|
|
230
|
+
export declare function formatCheckpointAge(ms: number): string;
|
|
231
|
+
/**
|
|
232
|
+
* Find the newest `lora-checkpoint-<ts>.json` without loading it. Pure disk
|
|
233
|
+
* scan — safe to call from read-only status surfaces. Returns null when no
|
|
234
|
+
* checkpoint exists or the directory is unreadable.
|
|
235
|
+
*/
|
|
236
|
+
export declare function latestCheckpointInfo(): CheckpointInfo | null;
|
|
237
|
+
/**
|
|
238
|
+
* Flywheel entry point: locate the newest trained checkpoint and load it into
|
|
239
|
+
* `adapter` via the adapter's existing loadCheckpoint path, so routing /
|
|
240
|
+
* adaptation benefits from prior `neural train` runs.
|
|
241
|
+
*
|
|
242
|
+
* Contract: lazy (call on first adaptation use, never at CLI startup),
|
|
243
|
+
* non-fatal on any failure, and kill-switchable via
|
|
244
|
+
* CLAUDE_FLOW_NO_CHECKPOINT_AUTOLOAD=1.
|
|
245
|
+
*/
|
|
246
|
+
export declare function loadLatestCheckpoint(adapter: LoRAAdapter): Promise<{
|
|
247
|
+
loaded: boolean;
|
|
248
|
+
path?: string;
|
|
249
|
+
ageMs?: number;
|
|
250
|
+
}>;
|
|
216
251
|
/**
|
|
217
252
|
* Get or create singleton LoRA adapter instance
|
|
218
253
|
*/
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* @module lora-adapter
|
|
19
19
|
*/
|
|
20
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
21
21
|
import { dirname, join } from 'path';
|
|
22
22
|
import { createRequire } from 'module';
|
|
23
23
|
// ============================================================================
|
|
@@ -527,6 +527,105 @@ export class LoRAAdapter {
|
|
|
527
527
|
}
|
|
528
528
|
}
|
|
529
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Directories `neural train` writes checkpoints to. It only ever writes to
|
|
532
|
+
* `<cwd>/.claude-flow/neural` (see commands/neural.ts + services/native-training.ts),
|
|
533
|
+
* so that is the single source of truth — kept in a helper so the discovery
|
|
534
|
+
* and status surfaces stay consistent.
|
|
535
|
+
*/
|
|
536
|
+
function checkpointDirs() {
|
|
537
|
+
return [join(process.cwd(), '.claude-flow', 'neural')];
|
|
538
|
+
}
|
|
539
|
+
/** Compact relative-age label. Most-significant unit only. */
|
|
540
|
+
export function formatCheckpointAge(ms) {
|
|
541
|
+
const s = Math.floor(ms / 1000);
|
|
542
|
+
if (s < 60)
|
|
543
|
+
return `${s}s ago`;
|
|
544
|
+
const m = Math.floor(s / 60);
|
|
545
|
+
if (m < 60)
|
|
546
|
+
return `${m}m ago`;
|
|
547
|
+
const h = Math.floor(m / 60);
|
|
548
|
+
if (h < 24)
|
|
549
|
+
return `${h}h ago`;
|
|
550
|
+
const d = Math.floor(h / 24);
|
|
551
|
+
return `${d}d ago`;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Find the newest `lora-checkpoint-<ts>.json` without loading it. Pure disk
|
|
555
|
+
* scan — safe to call from read-only status surfaces. Returns null when no
|
|
556
|
+
* checkpoint exists or the directory is unreadable.
|
|
557
|
+
*/
|
|
558
|
+
export function latestCheckpointInfo() {
|
|
559
|
+
try {
|
|
560
|
+
let best = null;
|
|
561
|
+
for (const dir of checkpointDirs()) {
|
|
562
|
+
if (!existsSync(dir))
|
|
563
|
+
continue;
|
|
564
|
+
for (const f of readdirSync(dir)) {
|
|
565
|
+
const match = /^lora-checkpoint-(\d+)\.json$/.exec(f);
|
|
566
|
+
if (!match)
|
|
567
|
+
continue;
|
|
568
|
+
const ts = Number(match[1]);
|
|
569
|
+
if (!Number.isFinite(ts))
|
|
570
|
+
continue;
|
|
571
|
+
if (!best || ts > best.ts)
|
|
572
|
+
best = { path: join(dir, f), filename: f, ts };
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (!best)
|
|
576
|
+
return null;
|
|
577
|
+
const ageMs = Math.max(0, Date.now() - best.ts);
|
|
578
|
+
return { path: best.path, filename: best.filename, ageMs, ageLabel: formatCheckpointAge(ageMs) };
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Validate a file looks like a ruvllm training checkpoint before loading it,
|
|
586
|
+
* so a stray/corrupt JSON in the neural dir can't poison routing weights.
|
|
587
|
+
* Accepts both the JS-fallback envelope ({A,B,scaling}) and native ruvllm
|
|
588
|
+
* checkpoint shapes (weights/adapter/epoch/step/loss/version/index markers).
|
|
589
|
+
*/
|
|
590
|
+
function isCheckpointEnvelope(path) {
|
|
591
|
+
try {
|
|
592
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
593
|
+
if (!data || typeof data !== 'object')
|
|
594
|
+
return false;
|
|
595
|
+
const obj = data;
|
|
596
|
+
if (Array.isArray(obj.A) && Array.isArray(obj.B))
|
|
597
|
+
return true;
|
|
598
|
+
return ['weights', 'adapter', 'epoch', 'step', 'loss', 'version', 'index'].some((k) => k in obj);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Flywheel entry point: locate the newest trained checkpoint and load it into
|
|
606
|
+
* `adapter` via the adapter's existing loadCheckpoint path, so routing /
|
|
607
|
+
* adaptation benefits from prior `neural train` runs.
|
|
608
|
+
*
|
|
609
|
+
* Contract: lazy (call on first adaptation use, never at CLI startup),
|
|
610
|
+
* non-fatal on any failure, and kill-switchable via
|
|
611
|
+
* CLAUDE_FLOW_NO_CHECKPOINT_AUTOLOAD=1.
|
|
612
|
+
*/
|
|
613
|
+
export async function loadLatestCheckpoint(adapter) {
|
|
614
|
+
if (process.env.CLAUDE_FLOW_NO_CHECKPOINT_AUTOLOAD === '1')
|
|
615
|
+
return { loaded: false };
|
|
616
|
+
try {
|
|
617
|
+
const info = latestCheckpointInfo();
|
|
618
|
+
if (!info)
|
|
619
|
+
return { loaded: false };
|
|
620
|
+
if (!isCheckpointEnvelope(info.path))
|
|
621
|
+
return { loaded: false, path: info.path, ageMs: info.ageMs };
|
|
622
|
+
const loaded = await adapter.loadCheckpoint(info.path);
|
|
623
|
+
return { loaded, path: info.path, ageMs: info.ageMs };
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return { loaded: false };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
530
629
|
// ============================================================================
|
|
531
630
|
// Singleton & Factory Functions
|
|
532
631
|
// ============================================================================
|
|
@@ -545,6 +644,14 @@ export async function getLoRAAdapter() {
|
|
|
545
644
|
initPromise = (async () => {
|
|
546
645
|
const adapter = new LoRAAdapter();
|
|
547
646
|
await adapter.initialize();
|
|
647
|
+
// Flywheel: fold in the newest trained checkpoint so routing/adaptation
|
|
648
|
+
// benefits from prior `neural train` runs. This singleton is constructed
|
|
649
|
+
// lazily on first adaptation use (NOT at CLI startup), so the checkpoint
|
|
650
|
+
// scan + ruvllm load stay off the hot `--help` path. Non-fatal.
|
|
651
|
+
try {
|
|
652
|
+
await loadLatestCheckpoint(adapter);
|
|
653
|
+
}
|
|
654
|
+
catch { /* auto-load is best-effort — never block adaptation */ }
|
|
548
655
|
loraInstance = adapter;
|
|
549
656
|
return adapter;
|
|
550
657
|
})();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native training via @ruvector/ruvllm's TrainingPipeline (#2549 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* `neural train` historically trained only on the RuVector WASM path
|
|
5
|
+
* (MicroLoRA + InfoNCE) and its "checkpoint" was a freshly-constructed
|
|
6
|
+
* adapter's weights — the native TrainingPipeline (real epochs, loss
|
|
7
|
+
* history, early stopping, EWC registration, disk checkpoints since
|
|
8
|
+
* ruvllm 2.5.7) was never exercised. This service routes the LoRA
|
|
9
|
+
* training leg through it.
|
|
10
|
+
*
|
|
11
|
+
* Batch formulation: pattern-alignment pairs — input = embedding[i],
|
|
12
|
+
* target = embedding[i+1 mod n], quality 1.0 — the MSE analogue of the
|
|
13
|
+
* WASM path's anchor→positive contrastive objective (adjacent training
|
|
14
|
+
* items belong to the same pattern family by construction).
|
|
15
|
+
*
|
|
16
|
+
* Graceful: returns null when @ruvector/ruvllm is absent or anything
|
|
17
|
+
* throws — callers fall back to the WASM path.
|
|
18
|
+
*/
|
|
19
|
+
export interface NativeTrainingResult {
|
|
20
|
+
epochs: number;
|
|
21
|
+
steps: number;
|
|
22
|
+
finalLoss: number;
|
|
23
|
+
bestValLoss: number | null;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
earlyStopped: boolean;
|
|
26
|
+
checkpointPath?: string;
|
|
27
|
+
checkpointBytes?: number;
|
|
28
|
+
/** True when training resumed from a --resume checkpoint. */
|
|
29
|
+
resumed?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* How the resume happened: 'resumeFrom' (ruvllm >=2.6.0 — restores epoch
|
|
32
|
+
* position + optimizer state) or 'loadCheckpoint' (2.5.7 fallback —
|
|
33
|
+
* weights only, training restarts from epoch 0).
|
|
34
|
+
*/
|
|
35
|
+
resumeMode?: 'resumeFrom' | 'loadCheckpoint';
|
|
36
|
+
}
|
|
37
|
+
export interface NativeTrainingOptions {
|
|
38
|
+
embeddings: Float32Array[];
|
|
39
|
+
epochs: number;
|
|
40
|
+
batchSize: number;
|
|
41
|
+
learningRate: number;
|
|
42
|
+
dim: number;
|
|
43
|
+
/**
|
|
44
|
+
* Validation holdout fraction (0..1). >0 makes the pipeline report
|
|
45
|
+
* bestValLoss + early-stopping; 0 (or omitted) disables validation.
|
|
46
|
+
* validationSplit exists in ruvllm 2.5.7 — no version gate needed.
|
|
47
|
+
*/
|
|
48
|
+
validationSplit?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Resume weights (and, on >=2.6.0, epoch position) from this checkpoint
|
|
51
|
+
* BEFORE training. A missing/invalid path throws ResumeFailedError — an
|
|
52
|
+
* explicit --resume must fail loudly, never silently fresh-train.
|
|
53
|
+
*/
|
|
54
|
+
resumeFrom?: string;
|
|
55
|
+
/** When set, the TRAINED pipeline checkpoints here (ruvllm >=2.5.7). */
|
|
56
|
+
checkpointPath?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Thrown when an explicit --resume checkpoint cannot be found or loaded.
|
|
60
|
+
* Distinct from the generic "native unavailable → null" degradation so the
|
|
61
|
+
* caller can surface a loud, exit-1 failure rather than fresh training.
|
|
62
|
+
*/
|
|
63
|
+
export declare class ResumeFailedError extends Error {
|
|
64
|
+
constructor(message: string);
|
|
65
|
+
}
|
|
66
|
+
export declare function nativeTrainingAvailable(): boolean;
|
|
67
|
+
export declare function runNativeTraining(opts: NativeTrainingOptions): Promise<NativeTrainingResult | null>;
|
|
68
|
+
//# sourceMappingURL=native-training.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native training via @ruvector/ruvllm's TrainingPipeline (#2549 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* `neural train` historically trained only on the RuVector WASM path
|
|
5
|
+
* (MicroLoRA + InfoNCE) and its "checkpoint" was a freshly-constructed
|
|
6
|
+
* adapter's weights — the native TrainingPipeline (real epochs, loss
|
|
7
|
+
* history, early stopping, EWC registration, disk checkpoints since
|
|
8
|
+
* ruvllm 2.5.7) was never exercised. This service routes the LoRA
|
|
9
|
+
* training leg through it.
|
|
10
|
+
*
|
|
11
|
+
* Batch formulation: pattern-alignment pairs — input = embedding[i],
|
|
12
|
+
* target = embedding[i+1 mod n], quality 1.0 — the MSE analogue of the
|
|
13
|
+
* WASM path's anchor→positive contrastive objective (adjacent training
|
|
14
|
+
* items belong to the same pattern family by construction).
|
|
15
|
+
*
|
|
16
|
+
* Graceful: returns null when @ruvector/ruvllm is absent or anything
|
|
17
|
+
* throws — callers fall back to the WASM path.
|
|
18
|
+
*/
|
|
19
|
+
import { createRequire } from 'module';
|
|
20
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
21
|
+
import { dirname } from 'path';
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when an explicit --resume checkpoint cannot be found or loaded.
|
|
24
|
+
* Distinct from the generic "native unavailable → null" degradation so the
|
|
25
|
+
* caller can surface a loud, exit-1 failure rather than fresh training.
|
|
26
|
+
*/
|
|
27
|
+
export class ResumeFailedError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'ResumeFailedError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function nativeTrainingAvailable() {
|
|
34
|
+
try {
|
|
35
|
+
createRequire(import.meta.url).resolve('@ruvector/ruvllm');
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function runNativeTraining(opts) {
|
|
43
|
+
const { embeddings, epochs, batchSize, learningRate, dim, validationSplit, resumeFrom, checkpointPath } = opts;
|
|
44
|
+
if (embeddings.length < 2)
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
const req = createRequire(import.meta.url);
|
|
48
|
+
const ruvllm = req('@ruvector/ruvllm');
|
|
49
|
+
const pipelineConfig = {
|
|
50
|
+
learningRate,
|
|
51
|
+
batchSize,
|
|
52
|
+
epochs,
|
|
53
|
+
inputDim: dim,
|
|
54
|
+
outputDim: dim,
|
|
55
|
+
};
|
|
56
|
+
// 0 (or omitted) disables validation; only pass a real holdout through.
|
|
57
|
+
if (typeof validationSplit === 'number' && validationSplit > 0) {
|
|
58
|
+
pipelineConfig.validationSplit = validationSplit;
|
|
59
|
+
}
|
|
60
|
+
const pipeline = new ruvllm.TrainingPipeline(pipelineConfig);
|
|
61
|
+
// Resume BEFORE training. Prefer resumeFrom() (2.6.0 — epoch position +
|
|
62
|
+
// optimizer state); fall back to loadCheckpoint() (2.5.7 — weights only).
|
|
63
|
+
// Any failure with an explicit --resume is loud (ResumeFailedError),
|
|
64
|
+
// never silent fresh training.
|
|
65
|
+
let resumed = false;
|
|
66
|
+
let resumeMode;
|
|
67
|
+
if (resumeFrom) {
|
|
68
|
+
if (!existsSync(resumeFrom)) {
|
|
69
|
+
throw new ResumeFailedError(`--resume checkpoint not found: ${resumeFrom}`);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
if (typeof pipeline.resumeFrom === 'function') {
|
|
73
|
+
pipeline.resumeFrom(resumeFrom);
|
|
74
|
+
resumeMode = 'resumeFrom';
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const ok = pipeline.loadCheckpoint(resumeFrom);
|
|
78
|
+
if (ok === false)
|
|
79
|
+
throw new Error('loadCheckpoint returned false');
|
|
80
|
+
resumeMode = 'loadCheckpoint';
|
|
81
|
+
}
|
|
82
|
+
resumed = true;
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
if (e instanceof ResumeFailedError)
|
|
86
|
+
throw e;
|
|
87
|
+
throw new ResumeFailedError(`--resume failed to load checkpoint ${resumeFrom}: ${e.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Pattern-alignment pairs, chunked into pipeline batches.
|
|
91
|
+
const inputs = [];
|
|
92
|
+
const targets = [];
|
|
93
|
+
const qualities = [];
|
|
94
|
+
for (let i = 0; i < embeddings.length; i++) {
|
|
95
|
+
inputs.push(Array.from(embeddings[i]));
|
|
96
|
+
targets.push(Array.from(embeddings[(i + 1) % embeddings.length]));
|
|
97
|
+
qualities.push(1.0);
|
|
98
|
+
}
|
|
99
|
+
for (let i = 0; i < inputs.length; i += batchSize) {
|
|
100
|
+
pipeline.addBatch(inputs.slice(i, i + batchSize), targets.slice(i, i + batchSize), qualities.slice(i, i + batchSize));
|
|
101
|
+
}
|
|
102
|
+
const r = pipeline.train();
|
|
103
|
+
// When validation is disabled the pipeline reports Infinity for
|
|
104
|
+
// bestValLoss — normalize any non-finite value to null so downstream
|
|
105
|
+
// "bestValLoss !== null ⇒ validation ran" holds for the results table.
|
|
106
|
+
const bestValLoss = typeof r.bestValLoss === 'number' && Number.isFinite(r.bestValLoss)
|
|
107
|
+
? r.bestValLoss
|
|
108
|
+
: null;
|
|
109
|
+
const result = {
|
|
110
|
+
epochs: r.epochs,
|
|
111
|
+
steps: r.steps,
|
|
112
|
+
finalLoss: r.finalLoss,
|
|
113
|
+
bestValLoss,
|
|
114
|
+
durationMs: r.durationMs,
|
|
115
|
+
earlyStopped: !!r.earlyStopped,
|
|
116
|
+
resumed,
|
|
117
|
+
resumeMode,
|
|
118
|
+
};
|
|
119
|
+
if (checkpointPath) {
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(dirname(checkpointPath), { recursive: true });
|
|
122
|
+
const saved = pipeline.saveCheckpoint(checkpointPath);
|
|
123
|
+
// <2.5.7 returns undefined and writes nothing — verify on disk.
|
|
124
|
+
if (existsSync(checkpointPath)) {
|
|
125
|
+
result.checkpointPath = checkpointPath;
|
|
126
|
+
result.checkpointBytes = saved?.bytes;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch { /* checkpoint is best-effort; training result stands */ }
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// An explicit --resume failure must propagate as a loud error; every
|
|
135
|
+
// other failure degrades to null so callers fall back to the WASM path.
|
|
136
|
+
if (err instanceof ResumeFailedError)
|
|
137
|
+
throw err;
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=native-training.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|