ruvnet / ruvnet/agentic-flow

All five attention mechanisms return K and ignore V entirely: native classes live on .default but are read off the namespace, so every constructor is undefined and it silently falls back to an unnormalized JS stub

Open
#191 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
812
Forks
175
Avg merge
2m
Merged PRs (30d)
3

Description

Summary

Every attention mechanism on EnhancedAgentDBWrapper returns K and ignores V entirely. Changing V has no effect on the output at all. This affects flashAttention, linearAttention, multiHeadAttention, hyperbolicAttention and moeAttention identically.

The underlying cause is that the native @ruvector/attention classes are imported off the module namespace when they actually live on its default export, so every native constructor is undefined, every mechanism silently degrades to the JS fallback, and that fallback does not implement attention correctly.

Version: agentic-flow 2.1.2, @ruvector/attention 0.1.32, Node v22.23.0, macOS 15.

Reproduce

const w = new EnhancedAgentDBWrapper({ enableAttention: true, attentionConfig: { type: 'flash' } });
await w.initialize();                                   // dimension 384

const D = 384, fill = v => Float32Array.from({ length: D }, () => v);
const Q = fill(1), K = fill(7), V = fill(3);            // maximally distinguishable

for (const m of ['linearAttention','flashAttention','multiHeadAttention','hyperbolicAttention','moeAttention']) {
  const r = await w[m](Q, K, V);
  console.log(m, Array.from(r.output).slice(0, 3));
}
linearAttention      out[0:3]=[7,7,7]   → K, not V
flashAttention       out[0:3]=[7,7,7]   → K, not V
multiHeadAttention   out[0:3]=[7,7,7]   → K, not V
hyperbolicAttention  out[0:3]=[7,7,7]   → K, not V
moeAttention         out[0:3]=[7,7,7]   → K, not V

V has no influence whatsoever:

const a = await w.flashAttention(Q, K, fill(3));
const b = await w.flashAttention(Q, K, fill(999));
// outputs byte-identical: [7,7,7] and [7,7,7]

Attention must return a weighted combination of V. Returning K means the values are never consulted.

Note this is easy to miss in a smoke test: if you pass the same array as both K and V (the natural thing to do when testing), the output looks correct because K === V. It only shows up when they differ.

Root cause 1 (confirmed): the native module is imported off the wrong object

dist/core/attention-native.js:

nativeAttention = await import('@ruvector/attention');       // namespace import
...
this.nativeInstance = new nativeAttention.MultiHeadAttention(this.hiddenDim, this.numHeads);  // line 49
this.nativeInstance = new nativeAttention.FlashAttention(this.hiddenDim);                     // line 98
this.nativeInstance = new nativeAttention.LinearAttention(this.hiddenDim, this.hiddenDim);    // line 139
this.nativeInstance = new nativeAttention.HyperbolicAttention(this.hiddenDim);                // line 169
this.nativeInstance = new nativeAttention.MoEAttention(this.hiddenDim);                       // line 205

But @ruvector/attention@0.1.32 exposes everything on default:

const ns = await import('@ruvector/attention');
Object.keys(ns)                      // [ 'default' ]      ← nothing else
typeof ns.MultiHeadAttention         // 'undefined'
typeof ns.default.MultiHeadAttention // 'function'
typeof ns.default.MoEAttention       // 'function'
typeof ns.default.AdamOptimizer      // 'function'

So every new nativeAttention.X(...) throws, the constructors are caught, and the wrapper falls through to the JS implementation. The console output confirms this — it prints ✅ Loaded @ruvector/attention NAPI module and then, on the very next line, AttentionService initialized (runtime: js). The success message refers to the import resolving, not to any class being usable.

One-line fix: nativeAttention = (await import('@ruvector/attention')).default;

This also explains #186, where hooks route logs TypeError: MoEAttention is not a constructor and TypeError: AdamOptimizer is not a constructor before falling back and reporting NaN% factors. Both classes exist — on .default.

Root cause 2 (partial): the JS fallback's core is not softmax attention

dist/core/attention-fallbacks.js, scaledDotProductAttention (line 11):

let score = 0;
for (let i = 0; i < dk; i++) score += query[i] * key[i];
score /= Math.sqrt(dk);
const expScore = Math.exp(score);
const weight = expScore;                    // "Simplified for single K,V pair"
const output = value.map(v => v * weight);

The softmax is never normalized — weight is raw exp(score) rather than exp(score) / Σexp(...), which for a single key must be exactly 1. With 384-dimensional inputs the dot product is large and exp() overflows the result by many orders of magnitude.

I have not isolated the exact line that produces K rather than a scaled V; the MultiHeadAttention.forward path projects per-head and reconcatenates, so more tracing is needed. Flagging that as unfinished rather than guessing. The empirical behaviour above is reproducible regardless of which line is responsible.

Impact

enableAttention: true is the one advanced feature of this wrapper that appears to work — it initialises cleanly, reports timings (AttentionService initialized in 7.28ms), and returns finite non-zero output. But the output is not a function of the values being attended over, so anything built on it (reranking, attentionSearch, coordinateAgents, the documented recommendation and research examples) is operating on a result unrelated to its inputs.

It also fails silently in the most convincing possible way: no error, plausible numbers, correct-looking metadata (mechanism: 'flash', runtime: 'js', executionTimeMs).

Suggested fix

  1. (await import('@ruvector/attention')).default in attention-native.js, and assert the constructors are functions before use — an undefined constructor should fail loudly, not degrade.
  2. Distinguish "native unavailable" from "native loaded" in the log. Printing ✅ Loaded @ruvector/attention NAPI module while runtime: js is actively misleading.
  3. Fix scaledDotProductAttention to normalize (for a single key the weight is 1), and add a test asserting that changing V changes the output — that single assertion would have caught this.
  4. Verify the fallback returns a combination of V for every mechanism, not K.

Related

  • #186 — hooks route NaN% factors; the MoEAttention/AdamOptimizer constructor failures there have the same namespace-vs-default cause.
  • #189 — agentic-flow/core and /coordination shipped but not exported; enableGNN: true silently degrades.
  • #190 — routeToExperts silently returns first-K when dimensions mismatch. That method routes through moeAttention, so it sits downstream of this.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with dist/core/attention-native.js and inspect how the dynamic import is used before each native attention constructor is created. Then read dist/core/attention-fallbacks.js and run the reported Q/K/V reproduction, adding coverage that changes V for every mechanism. Done means usable native constructors are detected, fallback logging distinguishes native from JS execution, and each path returns output influenced by V rather than K.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
backend, machine-learning, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.