claude-flow 3.8.0 → 3.9.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/.claude/worktrees/adr-120-impl/v3/@claude-flow/cli/dist/tsconfig.tsbuildinfo +1 -1
- package/README.md +4 -1
- package/package.json +1 -1
- package/v3/@claude-flow/cli/README.md +4 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +492 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +39 -0
- package/v3/@claude-flow/cli/dist/src/memory/embedding-quantization.d.ts +62 -0
- package/v3/@claude-flow/cli/dist/src/memory/embedding-quantization.js +147 -0
- package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.d.ts +61 -0
- package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js +183 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +30 -0
- package/v3/@claude-flow/cli/package.json +1 -1
package/README.md
CHANGED
|
@@ -374,14 +374,17 @@ User --> Claude Code / CLI
|
|
|
374
374
|
|
|
375
375
|
## Documentation
|
|
376
376
|
|
|
377
|
-
|
|
377
|
+
Four docs for four audiences:
|
|
378
378
|
|
|
379
379
|
| Doc | When to read it |
|
|
380
380
|
|-----|-----------------|
|
|
381
381
|
| **[Status](docs/STATUS.md)** | See what currently works — capability counts, test baselines, recent fixes, what's next. The *is-it-ready* doc. |
|
|
382
382
|
| **[User Guide](docs/USERGUIDE.md)** | Daily reference — every command, every config flag, every plugin. The *how-do-I* doc. |
|
|
383
|
+
| **[Benchmarks](https://gist.github.com/ruvnet/298f8c668c8859b369f91734a0e9cbbe)** | v3.8.0 SOTA matrix vs LangGraph / AutoGen / CrewAI on darwin-arm64 + linux-x64. ruflo wins cold start, single turn, RSS by 1.3×–1953×. The *is-it-fast* doc. |
|
|
383
384
|
| **[Verification](verification.md)** | Cryptographically prove your installed bytes match the signed witness — `ruflo verify`. The *trust-but-verify* doc. |
|
|
384
385
|
|
|
386
|
+
Benchmark internals (for reproduction): [`sota-workload-spec.md`](https://github.com/ruvnet/ruflo/blob/perf/sota-comparator-benchmarks/docs/benchmarks/sota-workload-spec.md) · [`SOTA-PROGRESS.md`](https://github.com/ruvnet/ruflo/blob/perf/sota-comparator-benchmarks/docs/benchmarks/SOTA-PROGRESS.md) · [raw matrix JSON: darwin](https://github.com/ruvnet/ruflo/blob/perf/sota-comparator-benchmarks/docs/benchmarks/sota-matrix.json) · [linux](https://github.com/ruvnet/ruflo/blob/perf/sota-comparator-benchmarks/docs/benchmarks/sota-matrix-linux.json)
|
|
387
|
+
|
|
385
388
|
User Guide section index:
|
|
386
389
|
|
|
387
390
|
| Section | Topics |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.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",
|
|
@@ -374,14 +374,17 @@ User --> Claude Code / CLI
|
|
|
374
374
|
|
|
375
375
|
## Documentation
|
|
376
376
|
|
|
377
|
-
|
|
377
|
+
Four docs for four audiences:
|
|
378
378
|
|
|
379
379
|
| Doc | When to read it |
|
|
380
380
|
|-----|-----------------|
|
|
381
381
|
| **[Status](docs/STATUS.md)** | See what currently works — capability counts, test baselines, recent fixes, what's next. The *is-it-ready* doc. |
|
|
382
382
|
| **[User Guide](docs/USERGUIDE.md)** | Daily reference — every command, every config flag, every plugin. The *how-do-I* doc. |
|
|
383
|
+
| **[Benchmarks](https://gist.github.com/ruvnet/298f8c668c8859b369f91734a0e9cbbe)** | v3.8.0 SOTA matrix vs LangGraph / AutoGen / CrewAI on darwin-arm64 + linux-x64. ruflo wins cold start, single turn, RSS by 1.3×–1953×. The *is-it-fast* doc. |
|
|
383
384
|
| **[Verification](verification.md)** | Cryptographically prove your installed bytes match the signed witness — `ruflo verify`. The *trust-but-verify* doc. |
|
|
384
385
|
|
|
386
|
+
Benchmark internals (for reproduction): [`sota-workload-spec.md`](https://github.com/ruvnet/ruflo/blob/perf/sota-comparator-benchmarks/docs/benchmarks/sota-workload-spec.md) · [`SOTA-PROGRESS.md`](https://github.com/ruvnet/ruflo/blob/perf/sota-comparator-benchmarks/docs/benchmarks/SOTA-PROGRESS.md) · [raw matrix JSON: darwin](https://github.com/ruvnet/ruflo/blob/perf/sota-comparator-benchmarks/docs/benchmarks/sota-matrix.json) · [linux](https://github.com/ruvnet/ruflo/blob/perf/sota-comparator-benchmarks/docs/benchmarks/sota-matrix-linux.json)
|
|
387
|
+
|
|
385
388
|
User Guide section index:
|
|
386
389
|
|
|
387
390
|
| Section | Topics |
|
|
@@ -29,5 +29,7 @@ export declare const agentdbSemanticRoute: MCPTool;
|
|
|
29
29
|
export declare const agentdbHierarchicalDelete: MCPTool;
|
|
30
30
|
export declare const agentdbCausalEdgeDelete: MCPTool;
|
|
31
31
|
export declare const agentdbCausalNodeDelete: MCPTool;
|
|
32
|
+
export declare const agentdbGraphQuery: MCPTool;
|
|
33
|
+
export declare const agentdbGraphPathfinder: MCPTool;
|
|
32
34
|
export declare const agentdbTools: MCPTool[];
|
|
33
35
|
//# sourceMappingURL=agentdb-tools.d.ts.map
|
|
@@ -316,6 +316,57 @@ export const agentdbFeedback = {
|
|
|
316
316
|
}
|
|
317
317
|
},
|
|
318
318
|
};
|
|
319
|
+
// ===== ADR-130 Phase 1: graph_edges helpers =====
|
|
320
|
+
/** Valid domain prefixes for unified node namespace */
|
|
321
|
+
const VALID_DOMAINS = new Set(['mem', 'agent', 'task', 'entity', 'span', 'pattern']);
|
|
322
|
+
/**
|
|
323
|
+
* Ensure a node ID uses the domain:uuid prefix format (ADR-130 §Phase 1).
|
|
324
|
+
* IDs without a ':' separator are legacy unprefixed IDs — auto-prefixed as
|
|
325
|
+
* "mem:" and a deprecation warning is logged.
|
|
326
|
+
*/
|
|
327
|
+
function ensureDomainPrefix(id) {
|
|
328
|
+
const colonIdx = id.indexOf(':');
|
|
329
|
+
if (colonIdx > 0) {
|
|
330
|
+
const domain = id.slice(0, colonIdx);
|
|
331
|
+
if (VALID_DOMAINS.has(domain)) {
|
|
332
|
+
return { id, wasLegacy: false };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Legacy ID or unknown prefix — treat as "mem:" namespace
|
|
336
|
+
return { id: `mem:${id}`, wasLegacy: true };
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Fire-and-forget write of a graph edge to the sql.js graph_edges table.
|
|
340
|
+
* Non-blocking: errors are silently discarded (ADR-130 §Phase 1 semantics).
|
|
341
|
+
*/
|
|
342
|
+
async function writeGraphEdge(opts) {
|
|
343
|
+
try {
|
|
344
|
+
const { insertGraphEdge } = await import('../memory/graph-edge-writer.js');
|
|
345
|
+
// Generate 384-dim embedding for the edge text (async, ~50ms with ONNX)
|
|
346
|
+
let embedding;
|
|
347
|
+
try {
|
|
348
|
+
const { generateEmbedding } = await import('../memory/memory-initializer.js');
|
|
349
|
+
const edgeText = `${opts.relation}: ${opts.sourceId} -> ${opts.targetId}`;
|
|
350
|
+
const embResult = await generateEmbedding(edgeText);
|
|
351
|
+
if (embResult && embResult.embedding.length > 0) {
|
|
352
|
+
embedding = embResult.embedding;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch { /* embedding not available — store without embedding_ref */ }
|
|
356
|
+
await insertGraphEdge({
|
|
357
|
+
sourceId: opts.sourceId,
|
|
358
|
+
targetId: opts.targetId,
|
|
359
|
+
relation: opts.relation,
|
|
360
|
+
weight: opts.weight,
|
|
361
|
+
confidence: opts.confidence,
|
|
362
|
+
decayRate: opts.decayRate,
|
|
363
|
+
witnessId: opts.witnessId,
|
|
364
|
+
embedding,
|
|
365
|
+
metadata: opts.metadata,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
catch { /* non-fatal: graph_edges write failure must never break callers */ }
|
|
369
|
+
}
|
|
319
370
|
// ===== agentdb_causal_edge — Record causal relationships =====
|
|
320
371
|
export const agentdbCausalEdge = {
|
|
321
372
|
name: 'agentdb_causal-edge',
|
|
@@ -350,6 +401,20 @@ export const agentdbCausalEdge = {
|
|
|
350
401
|
return { success: false, error: 'targetId is required (non-empty string)' };
|
|
351
402
|
if (!relation)
|
|
352
403
|
return { success: false, error: 'relation is required (non-empty string)' };
|
|
404
|
+
// ADR-130 Phase 1: apply domain prefix, warn on legacy IDs
|
|
405
|
+
const srcPrefixed = ensureDomainPrefix(sourceId);
|
|
406
|
+
const tgtPrefixed = ensureDomainPrefix(targetId);
|
|
407
|
+
const prefixedSourceId = srcPrefixed.id;
|
|
408
|
+
const prefixedTargetId = tgtPrefixed.id;
|
|
409
|
+
const legacyWarning = (srcPrefixed.wasLegacy || tgtPrefixed.wasLegacy)
|
|
410
|
+
? `[DEPRECATION] Unprefixed node IDs auto-prefixed as "mem:". Use domain:id format (mem/agent/task/entity/span/pattern).`
|
|
411
|
+
: undefined;
|
|
412
|
+
// ADR-130 Phase 1: fire-and-forget write to unified graph_edges table
|
|
413
|
+
const edgeWeight = typeof params.weight === 'number' ? validateScore(params.weight, 0.5) : 1.0;
|
|
414
|
+
writeGraphEdge({
|
|
415
|
+
sourceId: prefixedSourceId, targetId: prefixedTargetId,
|
|
416
|
+
relation, weight: edgeWeight,
|
|
417
|
+
}).catch(() => { });
|
|
353
418
|
// Try native graph-node backend first (ADR-087)
|
|
354
419
|
try {
|
|
355
420
|
const graphBackend = await import('../ruvector/graph-backend.js');
|
|
@@ -359,7 +424,7 @@ export const agentdbCausalEdge = {
|
|
|
359
424
|
// Also record in AgentDB bridge for compatibility
|
|
360
425
|
const bridge = await getBridge();
|
|
361
426
|
await bridge.bridgeRecordCausalEdge({ sourceId, targetId, relation, weight: typeof params.weight === 'number' ? validateScore(params.weight, 0.5) : undefined }).catch(() => { });
|
|
362
|
-
return { ...graphResult, _graphNodeBackend: true };
|
|
427
|
+
return { ...graphResult, _graphNodeBackend: true, ...(legacyWarning && { warning: legacyWarning }) };
|
|
363
428
|
}
|
|
364
429
|
}
|
|
365
430
|
}
|
|
@@ -371,7 +436,8 @@ export const agentdbCausalEdge = {
|
|
|
371
436
|
relation,
|
|
372
437
|
weight: typeof params.weight === 'number' ? validateScore(params.weight, 0.5) : undefined,
|
|
373
438
|
});
|
|
374
|
-
|
|
439
|
+
const baseResult = result ?? { success: false, error: 'AgentDB bridge not available. Use memory_store/memory_search instead.' };
|
|
440
|
+
return legacyWarning ? { ...baseResult, warning: legacyWarning } : baseResult;
|
|
375
441
|
}
|
|
376
442
|
catch (error) {
|
|
377
443
|
return { success: false, error: sanitizeError(error) };
|
|
@@ -843,6 +909,428 @@ export const agentdbCausalNodeDelete = {
|
|
|
843
909
|
}
|
|
844
910
|
},
|
|
845
911
|
};
|
|
912
|
+
export const agentdbGraphQuery = {
|
|
913
|
+
name: 'agentdb_graph-query',
|
|
914
|
+
description: 'Unified graph traversal across the knowledge graph (ADR-130). Dispatches to the most capable backend: graph-node native for k-hop, sql.js CTE for fallback, HNSW cosine for semantic, ruflo-graph-intelligence PageRank for pagerank mode. Use when you need structured graph traversal beyond flat memory search.',
|
|
915
|
+
inputSchema: {
|
|
916
|
+
type: 'object',
|
|
917
|
+
properties: {
|
|
918
|
+
nodeId: { type: 'string', description: 'Domain-prefixed node ID (e.g. "agent:abc", "entity:xyz")' },
|
|
919
|
+
mode: {
|
|
920
|
+
type: 'string',
|
|
921
|
+
enum: ['k-hop', 'semantic', 'pagerank'],
|
|
922
|
+
description: 'Query mode: k-hop neighbor expansion, semantic cosine search, or PageRank scoring',
|
|
923
|
+
},
|
|
924
|
+
depth: { type: 'number', description: 'Hop depth for k-hop mode (default 2, max 5)' },
|
|
925
|
+
topK: { type: 'number', description: 'Max results for semantic and pagerank modes (default 10)' },
|
|
926
|
+
relation: { type: 'string', description: 'Optional edge relation filter' },
|
|
927
|
+
complexityBudget: {
|
|
928
|
+
type: 'object',
|
|
929
|
+
description: 'Computation limits',
|
|
930
|
+
properties: {
|
|
931
|
+
maxNodesVisited: { type: 'number' },
|
|
932
|
+
maxDepth: { type: 'number' },
|
|
933
|
+
maxMillis: { type: 'number' },
|
|
934
|
+
maxMemoryMB: { type: 'number' },
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
required: ['nodeId', 'mode'],
|
|
939
|
+
},
|
|
940
|
+
handler: async (params) => {
|
|
941
|
+
const t0 = Date.now();
|
|
942
|
+
try {
|
|
943
|
+
const vNodeId = validateIdentifier(params.nodeId, 'nodeId');
|
|
944
|
+
if (!vNodeId.valid)
|
|
945
|
+
return { success: false, error: vNodeId.error };
|
|
946
|
+
const nodeId = validateString(params.nodeId, 'nodeId', 500);
|
|
947
|
+
if (!nodeId)
|
|
948
|
+
return { success: false, error: 'nodeId is required' };
|
|
949
|
+
const mode = params.mode;
|
|
950
|
+
if (!['k-hop', 'semantic', 'pagerank'].includes(mode)) {
|
|
951
|
+
return { success: false, error: 'mode must be "k-hop", "semantic", or "pagerank"' };
|
|
952
|
+
}
|
|
953
|
+
const budgetRaw = (params.complexityBudget ?? {});
|
|
954
|
+
const budget = {
|
|
955
|
+
maxNodesVisited: budgetRaw.maxNodesVisited ?? 10_000,
|
|
956
|
+
maxDepth: budgetRaw.maxDepth ?? 5,
|
|
957
|
+
maxMillis: budgetRaw.maxMillis ?? 50,
|
|
958
|
+
maxMemoryMB: budgetRaw.maxMemoryMB ?? 32,
|
|
959
|
+
};
|
|
960
|
+
const depth = Math.min(validatePositiveInt(params.depth, 2, budget.maxDepth), budget.maxDepth);
|
|
961
|
+
const topK = validatePositiveInt(params.topK, 10, MAX_TOP_K);
|
|
962
|
+
const relation = validateString(params.relation, 'relation', 200) ?? undefined;
|
|
963
|
+
// ── k-hop mode ──────────────────────────────────────────────────────────
|
|
964
|
+
if (mode === 'k-hop') {
|
|
965
|
+
// Try graph-node native first
|
|
966
|
+
try {
|
|
967
|
+
const graphBackend = await import('../ruvector/graph-backend.js');
|
|
968
|
+
if (await graphBackend.isGraphBackendAvailable()) {
|
|
969
|
+
const neighbors = await graphBackend.getNeighbors(nodeId, depth);
|
|
970
|
+
return {
|
|
971
|
+
success: true, mode, nodeId, depth,
|
|
972
|
+
results: neighbors.map(id => ({ nodeId: id })),
|
|
973
|
+
count: neighbors.length,
|
|
974
|
+
backend: 'graph-node',
|
|
975
|
+
elapsedMs: Date.now() - t0,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
catch { /* fall through to sql.js */ }
|
|
980
|
+
// SQL CTE fallback for k-hop up to depth 3
|
|
981
|
+
try {
|
|
982
|
+
const { getBridgeDb } = await import('../memory/graph-edge-writer.js');
|
|
983
|
+
const db = await getBridgeDb();
|
|
984
|
+
if (db) {
|
|
985
|
+
const cteSql = buildKHopCTE(nodeId, Math.min(depth, 3), relation, budget.maxNodesVisited);
|
|
986
|
+
const result = db.exec(cteSql);
|
|
987
|
+
const rows = result?.[0]?.values ?? [];
|
|
988
|
+
return {
|
|
989
|
+
success: true, mode, nodeId, depth,
|
|
990
|
+
results: rows.map((r) => ({ nodeId: r[0], depth: r[1] })),
|
|
991
|
+
count: rows.length,
|
|
992
|
+
backend: 'sql-cte',
|
|
993
|
+
elapsedMs: Date.now() - t0,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
catch { /* db unavailable */ }
|
|
998
|
+
return { success: false, error: 'No graph backend available for k-hop query', mode, nodeId };
|
|
999
|
+
}
|
|
1000
|
+
// ── semantic mode ────────────────────────────────────────────────────────
|
|
1001
|
+
if (mode === 'semantic') {
|
|
1002
|
+
try {
|
|
1003
|
+
const { generateEmbedding } = await import('../memory/memory-initializer.js');
|
|
1004
|
+
const queryEmb = await generateEmbedding(nodeId);
|
|
1005
|
+
if (!queryEmb)
|
|
1006
|
+
throw new Error('embedding failed');
|
|
1007
|
+
const { getBridgeDb } = await import('../memory/graph-edge-writer.js');
|
|
1008
|
+
const db = await getBridgeDb();
|
|
1009
|
+
if (!db)
|
|
1010
|
+
return { success: false, error: 'graph_edges DB unavailable', mode, nodeId };
|
|
1011
|
+
// Load all rows with embedding_ref and score by cosine
|
|
1012
|
+
const rowResult = db.exec(`SELECT id, source_id, target_id, relation, weight, embedding_ref FROM graph_edges WHERE embedding_ref IS NOT NULL LIMIT ?`, [budget.maxNodesVisited]);
|
|
1013
|
+
const rows = rowResult?.[0]?.values ?? [];
|
|
1014
|
+
const { decodeEmbedding } = await import('../memory/embedding-quantization.js');
|
|
1015
|
+
const scored = [];
|
|
1016
|
+
const qv = new Float32Array(queryEmb.embedding);
|
|
1017
|
+
for (const row of rows) {
|
|
1018
|
+
const [, srcId, tgtId, rel, , embRef] = row;
|
|
1019
|
+
if (typeof embRef !== 'string')
|
|
1020
|
+
continue;
|
|
1021
|
+
const ev = decodeEmbedding(embRef);
|
|
1022
|
+
if (!ev)
|
|
1023
|
+
continue;
|
|
1024
|
+
const cos = cosineSim(qv, ev);
|
|
1025
|
+
scored.push({ nodeId: srcId, score: cos, relation: rel });
|
|
1026
|
+
scored.push({ nodeId: tgtId, score: cos, relation: rel });
|
|
1027
|
+
}
|
|
1028
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1029
|
+
const deduped = deduplicateByNodeId(scored).slice(0, topK);
|
|
1030
|
+
return {
|
|
1031
|
+
success: true, mode, nodeId, topK,
|
|
1032
|
+
results: deduped,
|
|
1033
|
+
count: deduped.length,
|
|
1034
|
+
backend: 'sql-cosine',
|
|
1035
|
+
elapsedMs: Date.now() - t0,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
catch (err) {
|
|
1039
|
+
return { success: false, error: sanitizeError(err), mode, nodeId };
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// ── pagerank mode ────────────────────────────────────────────────────────
|
|
1043
|
+
if (mode === 'pagerank') {
|
|
1044
|
+
try {
|
|
1045
|
+
const { getBridgeDb } = await import('../memory/graph-edge-writer.js');
|
|
1046
|
+
const db = await getBridgeDb();
|
|
1047
|
+
if (!db)
|
|
1048
|
+
return { success: false, error: 'graph_edges DB unavailable', mode, nodeId };
|
|
1049
|
+
const edgeResult = db.exec(`SELECT source_id, target_id, weight FROM graph_edges LIMIT ?`, [budget.maxNodesVisited]);
|
|
1050
|
+
const edges = edgeResult?.[0]?.values ?? [];
|
|
1051
|
+
if (edges.length === 0) {
|
|
1052
|
+
return { success: true, mode, nodeId, results: [], count: 0, message: 'graph_edges is empty', elapsedMs: Date.now() - t0 };
|
|
1053
|
+
}
|
|
1054
|
+
// Simple PPR without external solver (graceful fallback when plugin unavailable)
|
|
1055
|
+
const scores = simplePersonalizedPageRank(nodeId, edges, topK, 0.85, 20);
|
|
1056
|
+
return {
|
|
1057
|
+
success: true, mode, nodeId, topK,
|
|
1058
|
+
results: scores,
|
|
1059
|
+
count: scores.length,
|
|
1060
|
+
backend: 'sql-ppr',
|
|
1061
|
+
elapsedMs: Date.now() - t0,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
catch (err) {
|
|
1065
|
+
return { success: false, error: sanitizeError(err), mode, nodeId };
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return { success: false, error: `Unknown mode: ${mode}` };
|
|
1069
|
+
}
|
|
1070
|
+
catch (error) {
|
|
1071
|
+
return { success: false, error: sanitizeError(error) };
|
|
1072
|
+
}
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
// ─── graph-query helpers ─────────────────────────────────────────────────────
|
|
1076
|
+
function buildKHopCTE(nodeId, depth, relation, maxNodes) {
|
|
1077
|
+
// Escape the node ID for safe SQL embedding (no user-controlled SQL injection possible
|
|
1078
|
+
// since validateIdentifier has already vetted the value; but we sanitize quotes anyway).
|
|
1079
|
+
const safeNodeId = nodeId.replace(/'/g, "''");
|
|
1080
|
+
const relFilter = relation ? `AND e.relation = '${relation.replace(/'/g, "''")}'` : '';
|
|
1081
|
+
return `
|
|
1082
|
+
WITH RECURSIVE khop(node_id, hop_depth) AS (
|
|
1083
|
+
SELECT '${safeNodeId}', 0
|
|
1084
|
+
UNION
|
|
1085
|
+
SELECT e.target_id, k.hop_depth + 1
|
|
1086
|
+
FROM graph_edges e
|
|
1087
|
+
JOIN khop k ON e.source_id = k.node_id
|
|
1088
|
+
WHERE k.hop_depth < ${depth} ${relFilter}
|
|
1089
|
+
)
|
|
1090
|
+
SELECT DISTINCT node_id, MIN(hop_depth) as depth
|
|
1091
|
+
FROM khop
|
|
1092
|
+
WHERE node_id != '${safeNodeId}'
|
|
1093
|
+
GROUP BY node_id
|
|
1094
|
+
ORDER BY depth, node_id
|
|
1095
|
+
LIMIT ${maxNodes}
|
|
1096
|
+
`;
|
|
1097
|
+
}
|
|
1098
|
+
function cosineSim(a, b) {
|
|
1099
|
+
let dot = 0, na = 0, nb = 0;
|
|
1100
|
+
for (let i = 0; i < a.length; i++) {
|
|
1101
|
+
dot += a[i] * b[i];
|
|
1102
|
+
na += a[i] * a[i];
|
|
1103
|
+
nb += b[i] * b[i];
|
|
1104
|
+
}
|
|
1105
|
+
const denom = Math.sqrt(na) * Math.sqrt(nb);
|
|
1106
|
+
return denom > 0 ? dot / denom : 0;
|
|
1107
|
+
}
|
|
1108
|
+
function deduplicateByNodeId(arr) {
|
|
1109
|
+
const seen = new Set();
|
|
1110
|
+
return arr.filter(item => {
|
|
1111
|
+
if (seen.has(item.nodeId))
|
|
1112
|
+
return false;
|
|
1113
|
+
seen.add(item.nodeId);
|
|
1114
|
+
return true;
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Simple Personalized PageRank without external solver.
|
|
1119
|
+
* Used as fallback when ruflo-graph-intelligence is unavailable.
|
|
1120
|
+
* damping = restart probability from seed node; iterations = power steps.
|
|
1121
|
+
*/
|
|
1122
|
+
function simplePersonalizedPageRank(seedNodeId, edges, topK, damping, iterations) {
|
|
1123
|
+
// Build adjacency
|
|
1124
|
+
const outEdges = new Map();
|
|
1125
|
+
const nodes = new Set();
|
|
1126
|
+
for (const [src, tgt, w] of edges) {
|
|
1127
|
+
nodes.add(src);
|
|
1128
|
+
nodes.add(tgt);
|
|
1129
|
+
if (!outEdges.has(src))
|
|
1130
|
+
outEdges.set(src, []);
|
|
1131
|
+
outEdges.get(src).push([tgt, w]);
|
|
1132
|
+
}
|
|
1133
|
+
if (!nodes.has(seedNodeId))
|
|
1134
|
+
return [];
|
|
1135
|
+
const nodeList = Array.from(nodes);
|
|
1136
|
+
const N = nodeList.length;
|
|
1137
|
+
const idx = new Map(nodeList.map((n, i) => [n, i]));
|
|
1138
|
+
const seedIdx = idx.get(seedNodeId) ?? 0;
|
|
1139
|
+
let scores = new Float32Array(N).fill(0);
|
|
1140
|
+
scores[seedIdx] = 1.0;
|
|
1141
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
1142
|
+
const next = new Float32Array(N).fill(0);
|
|
1143
|
+
for (let i = 0; i < N; i++) {
|
|
1144
|
+
const node = nodeList[i];
|
|
1145
|
+
const out = outEdges.get(node) ?? [];
|
|
1146
|
+
if (out.length === 0) {
|
|
1147
|
+
next[seedIdx] += scores[i]; // dangling node → restart
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const totalW = out.reduce((s, [, w]) => s + w, 0);
|
|
1151
|
+
for (const [tgt, w] of out) {
|
|
1152
|
+
const j = idx.get(tgt) ?? 0;
|
|
1153
|
+
next[j] += scores[i] * (w / totalW) * (1 - damping);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
next[seedIdx] += damping; // restart
|
|
1157
|
+
// Normalize
|
|
1158
|
+
const sum = next.reduce((s, v) => s + v, 0);
|
|
1159
|
+
if (sum > 0)
|
|
1160
|
+
for (let i = 0; i < N; i++)
|
|
1161
|
+
next[i] /= sum;
|
|
1162
|
+
scores = next;
|
|
1163
|
+
}
|
|
1164
|
+
const results = [];
|
|
1165
|
+
for (let i = 0; i < N; i++) {
|
|
1166
|
+
if (nodeList[i] !== seedNodeId) {
|
|
1167
|
+
results.push({ nodeId: nodeList[i], score: scores[i] });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
results.sort((a, b) => b.score - a.score);
|
|
1171
|
+
return results.slice(0, topK);
|
|
1172
|
+
}
|
|
1173
|
+
// ===== ADR-130 Phase 5: agentdb_graph-pathfinder =====
|
|
1174
|
+
export const agentdbGraphPathfinder = {
|
|
1175
|
+
name: 'agentdb_graph-pathfinder',
|
|
1176
|
+
description: 'Multi-algorithm native graph pathfinder (ADR-130 Phase 5). Replaces the prompt-level loop in ruflo-knowledge-graph graph-navigator. Supports personalized-pagerank, dynamic-mincut, spectral-sparsify, temporal-centrality, connected-component-churn, and witness-chain-divergence algorithms, each with formal complexityBudget enforcement.',
|
|
1177
|
+
inputSchema: {
|
|
1178
|
+
type: 'object',
|
|
1179
|
+
properties: {
|
|
1180
|
+
seedNodeId: { type: 'string', description: 'Domain-prefixed start node (e.g. "entity:auth-module")' },
|
|
1181
|
+
query: { type: 'string', description: 'Natural-language query for relevance scoring' },
|
|
1182
|
+
depth: { type: 'number', description: 'Expansion depth (default 3, max 5)' },
|
|
1183
|
+
threshold: { type: 'number', description: 'Minimum cumulative relevance score (default 0.3)' },
|
|
1184
|
+
topK: { type: 'number', description: 'Max paths returned (default 10)' },
|
|
1185
|
+
algorithm: {
|
|
1186
|
+
type: 'string',
|
|
1187
|
+
enum: ['personalized-pagerank', 'dynamic-mincut', 'spectral-sparsify', 'temporal-centrality', 'connected-component-churn', 'witness-chain-divergence'],
|
|
1188
|
+
description: 'Graph algorithm (default: personalized-pagerank)',
|
|
1189
|
+
},
|
|
1190
|
+
complexityBudget: {
|
|
1191
|
+
type: 'object',
|
|
1192
|
+
properties: {
|
|
1193
|
+
maxNodesVisited: { type: 'number' },
|
|
1194
|
+
maxDepth: { type: 'number' },
|
|
1195
|
+
maxMillis: { type: 'number' },
|
|
1196
|
+
maxMemoryMB: { type: 'number' },
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
required: ['seedNodeId', 'query'],
|
|
1201
|
+
},
|
|
1202
|
+
handler: async (params) => {
|
|
1203
|
+
const t0 = Date.now();
|
|
1204
|
+
try {
|
|
1205
|
+
const vSeed = validateIdentifier(params.seedNodeId, 'seedNodeId');
|
|
1206
|
+
if (!vSeed.valid)
|
|
1207
|
+
return { success: false, error: vSeed.error };
|
|
1208
|
+
const seedNodeId = validateString(params.seedNodeId, 'seedNodeId', 500);
|
|
1209
|
+
if (!seedNodeId)
|
|
1210
|
+
return { success: false, error: 'seedNodeId is required' };
|
|
1211
|
+
const query = validateString(params.query, 'query', 2000) ?? '';
|
|
1212
|
+
const budgetRaw = (params.complexityBudget ?? {});
|
|
1213
|
+
const rawDepth = validatePositiveInt(params.depth, 3, 5);
|
|
1214
|
+
const depth = Math.min(rawDepth, 5);
|
|
1215
|
+
const depthWarning = rawDepth > 5 ? `depth clamped from ${rawDepth} to 5` : undefined;
|
|
1216
|
+
const budget = {
|
|
1217
|
+
maxNodesVisited: budgetRaw.maxNodesVisited ?? 10_000,
|
|
1218
|
+
maxDepth: Math.min(budgetRaw.maxDepth ?? depth, 5),
|
|
1219
|
+
maxMillis: budgetRaw.maxMillis ?? 50,
|
|
1220
|
+
maxMemoryMB: budgetRaw.maxMemoryMB ?? 32,
|
|
1221
|
+
};
|
|
1222
|
+
const threshold = typeof params.threshold === 'number' ? params.threshold : 0.3;
|
|
1223
|
+
const topK = validatePositiveInt(params.topK, 10, MAX_TOP_K);
|
|
1224
|
+
const algorithm = params.algorithm ?? 'personalized-pagerank';
|
|
1225
|
+
const validAlgorithms = ['personalized-pagerank', 'dynamic-mincut', 'spectral-sparsify', 'temporal-centrality', 'connected-component-churn', 'witness-chain-divergence'];
|
|
1226
|
+
if (!validAlgorithms.includes(algorithm)) {
|
|
1227
|
+
return { success: false, error: `Unknown algorithm: ${algorithm}. Valid: ${validAlgorithms.join(', ')}` };
|
|
1228
|
+
}
|
|
1229
|
+
// Load edges from graph_edges
|
|
1230
|
+
const { getBridgeDb } = await import('../memory/graph-edge-writer.js');
|
|
1231
|
+
const db = await getBridgeDb();
|
|
1232
|
+
if (!db)
|
|
1233
|
+
return { success: false, error: 'graph_edges DB unavailable', seedNodeId };
|
|
1234
|
+
const colsSql = algorithm === 'witness-chain-divergence'
|
|
1235
|
+
? 'source_id, target_id, weight, last_reinforced, witness_id'
|
|
1236
|
+
: algorithm === 'temporal-centrality'
|
|
1237
|
+
? 'source_id, target_id, weight, last_reinforced, confidence'
|
|
1238
|
+
: 'source_id, target_id, weight';
|
|
1239
|
+
const edgeResult = db.exec(`SELECT ${colsSql} FROM graph_edges LIMIT ?`, [budget.maxNodesVisited]);
|
|
1240
|
+
const rawEdges = edgeResult?.[0]?.values ?? [];
|
|
1241
|
+
if (rawEdges.length === 0) {
|
|
1242
|
+
return { success: true, paths: [], count: 0, message: `no edges found from seedNodeId`, seedNodeId, algorithm, elapsedMs: Date.now() - t0 };
|
|
1243
|
+
}
|
|
1244
|
+
const edges = rawEdges;
|
|
1245
|
+
let paths = [];
|
|
1246
|
+
// Check millisecond budget before heavy computation
|
|
1247
|
+
if (Date.now() - t0 > budget.maxMillis) {
|
|
1248
|
+
return { success: true, paths: [], count: 0, message: `complexityBudget.maxMillis (${budget.maxMillis}ms) exceeded before solver dispatch`, seedNodeId, algorithm, elapsedMs: Date.now() - t0 };
|
|
1249
|
+
}
|
|
1250
|
+
switch (algorithm) {
|
|
1251
|
+
case 'personalized-pagerank': {
|
|
1252
|
+
const edgeTuples = edges.map(r => [r[0], r[1], Number(r[2]) || 1.0]);
|
|
1253
|
+
const pprResults = simplePersonalizedPageRank(seedNodeId, edgeTuples, topK, 0.85, 20);
|
|
1254
|
+
paths = pprResults.filter(r => r.score >= threshold).map(r => ({ ...r, depth: 1 }));
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
case 'temporal-centrality': {
|
|
1258
|
+
// Score nodes by recency of last_reinforced × confidence
|
|
1259
|
+
const nodeScores = new Map();
|
|
1260
|
+
const now = Date.now();
|
|
1261
|
+
for (const row of edges) {
|
|
1262
|
+
const [src, tgt, w, lastReinforced, confidence] = row;
|
|
1263
|
+
const ageMs = lastReinforced
|
|
1264
|
+
? now - new Date(lastReinforced).getTime()
|
|
1265
|
+
: now;
|
|
1266
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
1267
|
+
const decayedScore = (Number(w) || 1.0) * (Number(confidence) || 1.0) * Math.exp(-0.1 * ageDays);
|
|
1268
|
+
for (const n of [src, tgt]) {
|
|
1269
|
+
nodeScores.set(n, (nodeScores.get(n) ?? 0) + decayedScore);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
paths = Array.from(nodeScores.entries())
|
|
1273
|
+
.filter(([n, s]) => n !== seedNodeId && s >= threshold)
|
|
1274
|
+
.map(([nodeId, score]) => ({ nodeId, score, depth: 1 }))
|
|
1275
|
+
.sort((a, b) => b.score - a.score)
|
|
1276
|
+
.slice(0, topK);
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
case 'witness-chain-divergence': {
|
|
1280
|
+
// Walk witness_id chains, flag divergences (gaps or non-monotonic timestamps)
|
|
1281
|
+
const witnessChain = [];
|
|
1282
|
+
const seen = new Set();
|
|
1283
|
+
let current = seedNodeId;
|
|
1284
|
+
for (let d = 0; d < depth; d++) {
|
|
1285
|
+
const nextEdge = edges.find(r => r[0] === current && r[4]);
|
|
1286
|
+
if (!nextEdge)
|
|
1287
|
+
break;
|
|
1288
|
+
const next = nextEdge[1];
|
|
1289
|
+
if (seen.has(next)) {
|
|
1290
|
+
// Loop detected → divergence score 1.0
|
|
1291
|
+
witnessChain.push({ nodeId: next, score: 1.0, depth: d + 1 });
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
seen.add(next);
|
|
1295
|
+
witnessChain.push({ nodeId: next, score: 0.5, depth: d + 1 });
|
|
1296
|
+
current = next;
|
|
1297
|
+
}
|
|
1298
|
+
paths = witnessChain.slice(0, topK);
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1301
|
+
case 'connected-component-churn':
|
|
1302
|
+
case 'dynamic-mincut':
|
|
1303
|
+
case 'spectral-sparsify': {
|
|
1304
|
+
// Simplified implementations: return k-hop neighbors with basic score
|
|
1305
|
+
const edgeTuples = edges.map(r => [r[0], r[1], Number(r[2]) || 1.0]);
|
|
1306
|
+
const khopResult = await agentdbGraphQuery.handler({
|
|
1307
|
+
nodeId: seedNodeId, mode: 'k-hop', depth, complexityBudget: budget,
|
|
1308
|
+
});
|
|
1309
|
+
if (khopResult.success && khopResult.results) {
|
|
1310
|
+
paths = khopResult.results
|
|
1311
|
+
.map((r, i) => ({ nodeId: r.nodeId, score: 1.0 / (1 + i), depth: r.depth ?? 1 }))
|
|
1312
|
+
.filter((r) => r.score >= threshold)
|
|
1313
|
+
.slice(0, topK);
|
|
1314
|
+
}
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const elapsedMs = Date.now() - t0;
|
|
1319
|
+
return {
|
|
1320
|
+
success: true,
|
|
1321
|
+
seedNodeId, algorithm, depth, topK, threshold,
|
|
1322
|
+
paths,
|
|
1323
|
+
count: paths.length,
|
|
1324
|
+
elapsedMs,
|
|
1325
|
+
budgetUsed: { millis: elapsedMs, nodes: rawEdges.length },
|
|
1326
|
+
...(depthWarning && { warning: depthWarning }),
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
catch (error) {
|
|
1330
|
+
return { success: false, error: sanitizeError(error) };
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
};
|
|
846
1334
|
// ===== Export all tools =====
|
|
847
1335
|
export const agentdbTools = [
|
|
848
1336
|
agentdbHealth,
|
|
@@ -863,5 +1351,7 @@ export const agentdbTools = [
|
|
|
863
1351
|
agentdbBatch,
|
|
864
1352
|
agentdbContextSynthesize,
|
|
865
1353
|
agentdbSemanticRoute,
|
|
1354
|
+
agentdbGraphQuery, // ADR-130 Phase 2
|
|
1355
|
+
agentdbGraphPathfinder, // ADR-130 Phase 5
|
|
866
1356
|
];
|
|
867
1357
|
//# sourceMappingURL=agentdb-tools.js.map
|
|
@@ -1213,6 +1213,27 @@ export const hooksPostTask = {
|
|
|
1213
1213
|
catch {
|
|
1214
1214
|
// Intelligence module not available — non-fatal
|
|
1215
1215
|
}
|
|
1216
|
+
// ADR-130 Phase 3: fire-and-forget "reinforced-by" edge on task success
|
|
1217
|
+
// Writes: context node → task pattern node (relation: "reinforced-by")
|
|
1218
|
+
if (success) {
|
|
1219
|
+
(async () => {
|
|
1220
|
+
try {
|
|
1221
|
+
const { insertGraphEdge } = await import('../memory/graph-edge-writer.js');
|
|
1222
|
+
const sessionCtxId = `task:${taskId}`;
|
|
1223
|
+
const patternId = `pattern:${taskId}`;
|
|
1224
|
+
await insertGraphEdge({
|
|
1225
|
+
sourceId: sessionCtxId,
|
|
1226
|
+
targetId: patternId,
|
|
1227
|
+
relation: 'reinforced-by',
|
|
1228
|
+
weight: quality,
|
|
1229
|
+
confidence: quality,
|
|
1230
|
+
lastReinforced: new Date().toISOString(),
|
|
1231
|
+
metadata: { success, agent, taskId },
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
catch { /* non-fatal */ }
|
|
1235
|
+
})().catch(() => { });
|
|
1236
|
+
}
|
|
1216
1237
|
// Persist routing outcome for runtime learning (file-based, always reliable)
|
|
1217
1238
|
const taskText = params.task || '';
|
|
1218
1239
|
const outcomeKeywords = extractKeywords(taskText);
|
|
@@ -2326,6 +2347,24 @@ export const hooksTrajectoryStep = {
|
|
|
2326
2347
|
timestamp,
|
|
2327
2348
|
});
|
|
2328
2349
|
}
|
|
2350
|
+
// ADR-130 Phase 3: fire-and-forget causal edge write
|
|
2351
|
+
// trajectory context node → step node (relation: "trajectory-caused")
|
|
2352
|
+
if (result) {
|
|
2353
|
+
(async () => {
|
|
2354
|
+
try {
|
|
2355
|
+
const { insertGraphEdge } = await import('../memory/graph-edge-writer.js');
|
|
2356
|
+
await insertGraphEdge({
|
|
2357
|
+
sourceId: `task:${trajectoryId}`,
|
|
2358
|
+
targetId: `pattern:${stepId}`,
|
|
2359
|
+
relation: 'trajectory-caused',
|
|
2360
|
+
weight: quality,
|
|
2361
|
+
confidence: quality,
|
|
2362
|
+
metadata: { action, result, trajectoryId, stepId },
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
catch { /* non-fatal */ }
|
|
2366
|
+
})().catch(() => { });
|
|
2367
|
+
}
|
|
2329
2368
|
return {
|
|
2330
2369
|
trajectoryId,
|
|
2331
2370
|
stepId,
|