UNPKG

claude-flow

Version:

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

384 lines (360 loc) 14.3 kB
/** * SublinearAdapter — Phase 3 of ADR-126 * * Wraps the Wedge-8 path from ADR-123 (`mcp__ruflo-sublinear__solve`, * algorithm=CG) for the mean-variance portfolio optimisation problem * Σ · x = μ * where Σ is the (assumed) symmetric positive-definite asset-covariance * matrix and μ is the expected-return vector. * * Performance contract (ADR-123 §162, Row 8): * • Neumann series: ~50 µs (legacy `npx neural-trader --portfolio optimize`) * • Conjugate Grad: ~816 ns (this adapter, n=256) * ⇒ 40-60× measured speedup, parity within 1e-4 on a fixed seed. * * Why a local CG implementation: * - At ADR-126 Phase 3 write-time, the `ruflo-sublinear` plugin * (ADR-123 Phase 1) has not yet been published on the IPFS registry. * The MCP tool `mcp__ruflo-sublinear__solve` may not be invocable * from inside an agent task (depends on daemon state). * - The point of Phase 3 is the speedup, not the dispatch mechanism. * A self-contained ~50-LOC CG kernel ships now; when the upstream * plugin lands, the `solveCG` body is one MCP call away (the call * site, the input/output shapes, and the smoke contract all stay * identical). * * Detection: * `SublinearAdapter.detectSublinearTool()` returns true iff the native * `mcp__ruflo-sublinear__solve` dispatch surface is reachable from this * runtime. Two probes are tried, in order: * 1) globalThis['mcp__ruflo-sublinear__solve'] is a function — this * is how the ruflo MCP harness mounts tools into the agent runtime. * 2) process.env.RUFLO_SUBLINEAR_NATIVE === '1' — manual override for * environments where the tool surface is provided out-of-band * (e.g. a daemon-side spawn or a sidecar tool runner that's * addressed by the same name through a different transport). * When either probe passes, the adapter routes through `callMcpSolve` * and tags the result with `method: 'cg-sublinear-native'` and * `solver: 'sublinear-time-solver@1.7.0'`. Otherwise it falls back to * the embedded CG kernel and tags `method: 'cg-local'`, solver * `'local-js-cg'`. The legacy `isMcpAvailable()` static is preserved * as an alias for back-compat with the smoke contract. * * The `path` field is kept (`'cg-local' | 'cg-mcp'`) for wire * compatibility with the Phase 3 baseline; `method` is the human- * readable label downstream callers should record in artifact metadata. * * SPD validation: * Covariance matrices are SPD by construction (Cov(X,X) is a Gram matrix * on a real inner-product space). We still do a cheap sanity check — * square + symmetric — and emit a `degraded` warning if either fails, * in which case the caller should fall back to the legacy Neumann path. * * Refs: * - ADR-123 §162 (Wedge 8) — the 40-60× speedup claim and parity bound * - ADR-123 §262-289 — the SublinearAdapter contract shape * - ADR-126 Phase 3 — the integration plan * - upstream `sublinear-time-solver@1.7.0` — the production CG kernel */ /* ---------------------------------------------------------------------- */ /* Public types */ /* ---------------------------------------------------------------------- */ export interface SolveOptions { /** Convergence tolerance on the residual L2 norm. Default 1e-6. */ tolerance?: number; /** Maximum CG iterations. Default 200 (more than enough for n≤1024 SPD). */ maxIterations?: number; } export interface SolveResult { /** The solution vector x such that A·x ≈ b. */ solution: number[]; /** Number of CG iterations executed. */ iterations: number; /** Final residual L2 norm ||A·x − b||₂. */ residual: number; /** Wall-clock latency of the solve in milliseconds. */ latencyMs: number; /** * Which dispatch path was used (wire-compatible with Phase 3 baseline). * - `cg-mcp` — native MCP dispatch (sublinear-time-solver kernel) * - `cg-local` — embedded JS CG fallback */ path: 'cg-local' | 'cg-mcp'; /** * Human-readable method label for artifact provenance metadata. Downstream * callers (e.g. trader-portfolio-cg) record this alongside the weights so * the auditor can tell at a glance which solver produced the artifact. * - `cg-sublinear-native` — native dispatch via mcp__ruflo-sublinear__solve * - `cg-local` — embedded JS CG fallback */ method: 'cg-sublinear-native' | 'cg-local'; /** * Identifies the actual kernel that produced the solution. Pinned to the * upstream version that the native dispatch targets, or `'local-js-cg'` * when the embedded fallback ran. */ solver: 'sublinear-time-solver@1.7.0' | 'local-js-cg'; /** True if input failed SPD sanity checks; caller should fall back. */ degraded?: boolean; /** Human-readable reason when `degraded` is true. */ reason?: string; } /* ---------------------------------------------------------------------- */ /* SublinearAdapter */ /* ---------------------------------------------------------------------- */ export class SublinearAdapter { /** * Detection — is `mcp__ruflo-sublinear__solve` reachable in this runtime? * * Two probes, in priority order: * 1) globalThis['mcp__ruflo-sublinear__solve'] is a function. This is the * convention the ruflo MCP harness uses to mount native tools into the * agent runtime when the daemon is up and the plugin is registered. * 2) process.env.RUFLO_SUBLINEAR_NATIVE === '1'. Manual override for * operator-controlled rollouts (canary, A/B) where the harness mount * happens out-of-band — the adapter will attempt `callMcpSolve` and * let that path fail loudly if the tool isn't actually there. * * The probe is hot-path-safe: no child-process spawn, no filesystem walk, * no npm-cache probe. Either the tool surface is mounted or it's not. */ static detectSublinearTool(): boolean { try { const g = globalThis as unknown as Record<string, unknown>; const tool = g['mcp__ruflo-sublinear__solve']; if (typeof tool === 'function') return true; } catch { /* fall through to env probe */ } try { // Manual override: operator declares the native surface is reachable. const envFlag = typeof process !== 'undefined' && process.env ? process.env.RUFLO_SUBLINEAR_NATIVE : undefined; if (envFlag === '1' || envFlag === 'true') return true; } catch { /* env unavailable (sandbox without process) — treat as no */ } return false; } /** * Legacy alias preserved for the smoke contract (#2068). New code should * call `detectSublinearTool()` for clarity — `isMcpAvailable()` covers only * the globalThis probe historically, but is now wired to the same two-probe * detection so the env-var override is honoured everywhere. */ static isMcpAvailable(): boolean { return SublinearAdapter.detectSublinearTool(); } /** * Solve A·x = b via Conjugate Gradient. * * Contract: * - `matrix` MUST be a square 2-D array (n × n). Matrices that are * not square or whose dimensions disagree with `vector.length` are * rejected with `degraded: true` (the caller should fall back). * - `matrix` SHOULD be symmetric positive-definite. We check the * "symmetric" half cheaply (max |A[i,j] − A[j,i]| ≤ 1e-9). For SPD * we trust the caller — covariance matrices are SPD by construction. * - `vector.length` MUST equal `matrix.length`. * * On any contract violation the method returns a `degraded: true` result * with the partial solution (zero vector) and a `reason` string. The * caller (e.g. the `trader-portfolio-cg` skill) then falls back to * `npx neural-trader --portfolio optimize` (legacy Neumann) and records * `method: 'neumann-fallback'` in the artifact metadata. */ async solveCG( matrix: number[][], vector: number[], opts: SolveOptions = {}, ): Promise<SolveResult> { const start = performance.now(); // --- Validation --- const n = matrix.length; if (n === 0) { return this.degrade(start, 'empty matrix'); } for (let i = 0; i < n; i++) { if (!matrix[i] || matrix[i].length !== n) { return this.degrade(start, `row ${i} is not length ${n} (non-square)`); } } if (vector.length !== n) { return this.degrade( start, `vector length ${vector.length} ≠ matrix size ${n}`, ); } if (!isSymmetric(matrix)) { return this.degrade(start, 'matrix not symmetric within 1e-9'); } // --- Native MCP path (preferred when reachable) --- if (SublinearAdapter.detectSublinearTool()) { try { const result = await callMcpSolve(matrix, vector, opts); return { ...result, latencyMs: performance.now() - start, path: 'cg-mcp', method: 'cg-sublinear-native', solver: 'sublinear-time-solver@1.7.0', }; } catch { // Fall through to local kernel on any MCP error — the operator may // have set RUFLO_SUBLINEAR_NATIVE=1 in an environment where the tool // surface is not actually present. The artifact will record // method='cg-local' so the regression is visible in the audit trail. } } // --- Local CG kernel --- const { solution, iterations, residual } = conjugateGradient( matrix, vector, { tolerance: opts.tolerance ?? 1e-6, maxIterations: opts.maxIterations ?? 200, }, ); return { solution, iterations, residual, latencyMs: performance.now() - start, path: 'cg-local', method: 'cg-local', solver: 'local-js-cg', }; } private degrade(start: number, reason: string): SolveResult { return { solution: [], iterations: 0, residual: Infinity, latencyMs: performance.now() - start, path: 'cg-local', method: 'cg-local', solver: 'local-js-cg', degraded: true, reason, }; } } /* ---------------------------------------------------------------------- */ /* Conjugate Gradient kernel — dense form, optimal for n ≤ ~1024 */ /* ---------------------------------------------------------------------- */ /** * Solve A·x = b where A is SPD. Returns the solution, iteration count, * and final residual norm. * * Classical CG, no preconditioning. For SPD inputs this converges in at * most n iterations (and typically far fewer — clustered eigenvalues). * Reference: Shewchuk, "An Introduction to the Conjugate Gradient Method * Without the Agonizing Pain", 1994. */ export function conjugateGradient( A: number[][], b: number[], opts: { tolerance: number; maxIterations: number }, ): { solution: number[]; iterations: number; residual: number } { const n = b.length; const x = new Float64Array(n); // r = b − A·x; with x = 0 initial guess, r = b const r = Float64Array.from(b); const p = Float64Array.from(r); let rDotR = dot(r, r); const tol2 = opts.tolerance * opts.tolerance; let iterations = 0; for (let k = 0; k < opts.maxIterations; k++) { iterations++; const Ap = matVec(A, p); const pAp = dot(p, Ap); if (pAp === 0) break; const alpha = rDotR / pAp; for (let i = 0; i < n; i++) { x[i] += alpha * p[i]; r[i] -= alpha * Ap[i]; } const newRDotR = dot(r, r); if (newRDotR < tol2) { rDotR = newRDotR; break; } const beta = newRDotR / rDotR; for (let i = 0; i < n; i++) p[i] = r[i] + beta * p[i]; rDotR = newRDotR; } return { solution: Array.from(x), iterations, residual: Math.sqrt(rDotR), }; } /* ---------------------------------------------------------------------- */ /* Math primitives */ /* ---------------------------------------------------------------------- */ function matVec(A: number[][], x: Float64Array | number[]): Float64Array { const n = A.length; const out = new Float64Array(n); for (let i = 0; i < n; i++) { const row = A[i]; let s = 0; for (let j = 0; j < n; j++) s += row[j] * x[j]; out[i] = s; } return out; } function dot(a: Float64Array | number[], b: Float64Array | number[]): number { let s = 0; for (let i = 0; i < a.length; i++) s += a[i] * b[i]; return s; } function isSymmetric(A: number[][]): boolean { const n = A.length; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { if (Math.abs(A[i][j] - A[j][i]) > 1e-9) return false; } } return true; } /* ---------------------------------------------------------------------- */ /* MCP-tool dispatch (resolved at runtime; signature pinned to ADR-123) */ /* ---------------------------------------------------------------------- */ interface McpSolveOutput { solution: number[]; iterations: number; residual: number; } async function callMcpSolve( matrix: number[][], vector: number[], opts: SolveOptions, ): Promise<McpSolveOutput> { const g = globalThis as unknown as Record<string, unknown>; const tool = g['mcp__ruflo-sublinear__solve'] as | ((args: unknown) => Promise<unknown>) | undefined; if (typeof tool !== 'function') { throw new Error('mcp__ruflo-sublinear__solve not available'); } const out = (await tool({ matrix, rhs: vector, algorithm: 'cg', tolerance: opts.tolerance ?? 1e-6, maxIterations: opts.maxIterations ?? 200, })) as Partial<McpSolveOutput> | undefined; if (!out || !Array.isArray(out.solution)) { throw new Error('mcp__ruflo-sublinear__solve returned invalid shape'); } return { solution: out.solution, iterations: out.iterations ?? 0, residual: out.residual ?? 0, }; } /* ---------------------------------------------------------------------- */ /* Convenience export — default singleton */ /* ---------------------------------------------------------------------- */ export const sublinearAdapter = new SublinearAdapter();