ruvnet / ruvnet/agentic-flow

extractAttentionWeights divides by sum instead of softmax: negative weights make weighted consensus leave the input range (values in [0,100] → consensus 508)

Open
#192 0 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

AttentionCoordinator.extractAttentionWeights() normalizes by dividing by the sum rather than applying a softmax. Attention output can be negative, so the resulting "weights" can be negative or greater than 1 while still summing to 1. They are not a probability distribution, and weightedConsensus() then uses them as one.

The observable consequence: a weighted consensus over values in [0, 100] returns 508. A weighted average cannot leave the convex hull of its inputs; this one does, silently, with no error.

All four public coordination methods route through it: coordinateAgents, topologyAwareCoordination, hierarchicalCoordination, and routeToExperts (via extractRoutingScores).

Version: agentic-flow 2.1.2, Node v22.23.0, macOS 15.

Reproduce

const w = new EnhancedAgentDBWrapper({ enableAttention: true, attentionConfig: { type: 'flash' } });
await w.initialize();
const c = new AttentionCoordinator(w.getAttentionService());

const D = 384, vec = f => Float32Array.from({ length: D }, (_, i) => f(i));
const mk = (id, f, v) => ({ agentId: id, agentType: 't', value: v, embedding: vec(f) });

const pi = [mk('pi-1', i => Math.sin(i/10), 100), mk('pi-2', i => Math.cos(i/10), 0)];
const ra = [mk('ra-1', i => Math.sin(i/7), 100), mk('ra-2', i => Math.cos(i/7), 0), mk('ra-3', i => Math.sin(i/3), 100)];

const r = await c.hierarchicalCoordination(pi, ra, -1.0);
weights   = [2.7798, -2.3772, 0.943, -1.703, 1.3574]     ← 2 of 5 negative, one > 2.7
values    = [100, 0, 100, 0, 100]
consensus = 508.0183032788398                            ← must lie within [0, 100]

The weights do sum to 1, which is what makes this survive a casual check. But two are negative, so the combination extrapolates far outside the range of the agents' own values.

Note a test that looks reasonable and proves nothing: give every agent the same value (say 100). Any weights summing to 1 then return exactly 100, negative or not. The inputs must differ for the defect to be visible — that is why this is easy to miss.

Cause

extractAttentionWeights(output, numAgents) {
    // Simplified: average across dimensions
    const dim = output.length / numAgents;
    const weights = [];
    for (let i = 0; i < numAgents; i++) {
        const slice = output.slice(i * dim, (i + 1) * dim);
        const avgWeight = slice.reduce((a, b) => a + b, 0) / slice.length;
        weights.push(avgWeight);
    }
    // Normalize to sum to 1
    const sum = weights.reduce((a, b) => a + b, 0);
    return weights.map((w) => w / sum);
}

Two problems:

  1. Divide-by-sum is not softmax. It enforces Σw = 1 but preserves sign and admits magnitudes above 1. Attention output over embeddings with negative components routinely produces negative slice means, as above.
  2. No guard on sum. If the positive and negative means roughly cancel, sum → 0 and every weight explodes. There is no epsilon and no check; the failure would be Infinity/NaN propagating into the consensus rather than an error.

Then weightedConsensus consumes them as if they were a distribution:

if (typeof outputs[0].value === 'number') {
    return outputs.reduce((sum, output, i) => sum + output.value * weights[i], 0);
}
if (Array.isArray(outputs[0].value)) { /* element-wise, same issue */ }

topAgents is likewise ranked by these values, so "top contributor" can be an agent whose weight is merely the largest signed number, not the largest contribution.

Affected call sites

coordinateAgents            → extractAttentionWeights   (line 86)
topologyAwareCoordination   → extractAttentionWeights   (line 167)
hierarchicalCoordination    → extractAttentionWeights   (line 209)
routeToExperts              → extractRoutingScores      → extractAttentionWeights (line 272)

extractRoutingScores partly masks the problem by applying Math.exp(s * 10) afterwards, which restores non-negativity — but it exponentiates values that have already been distorted by the bad normalization, and with temperature 10 the distortion is amplified before the softmax.

Suggested fix

Replace the divide-by-sum with a numerically stable softmax over the slice means:

const maxW = Math.max(...weights);
const exp  = weights.map(w => Math.exp(w - maxW));
const sum  = exp.reduce((a, b) => a + b, 0);
return exp.map(e => e / sum);

That guarantees w ∈ (0, 1) and Σw = 1, so the consensus stays inside the convex hull of the inputs and topAgents ranks by genuine contribution. extractRoutingScores can then keep its temperature term without compounding an existing distortion.

A regression test worth adding: assert that a consensus over values in [lo, hi] lands in [lo, hi]. That single assertion catches this class of bug permanently — and, importantly, uses differing values so it cannot pass vacuously.

Related

  • #190 — routeToExperts returns first-K unranked on dimension mismatch. Same file, different failure; both make routing output that looks plausible and isn't.
  • #191 — the wrapper discards V. Independent of this: hierarchicalCoordination calls attentionService.hyperbolicAttention directly, so its const V = K is legitimate self-attention, unaffected by the wrapper bug.

Separately, in the documented example for this method

The README-style snippet for hierarchicalCoordination builds agents as { agentId, output, embedding }, but AgentOutput is { agentId, agentType, embedding, value }. Run as written:

consensus.consensus            = undefined          // .value is undefined, falls to the object branch
topAgents.map(a => a.agentId)  = [undefined × 3]    // topAgents is already string[]

Both logged lines print undefined with no error. Renaming output:value: and adding agentType makes it return "Hypothesis A" correctly. Worth fixing in the docs alongside the examples in #188/#189.

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 by locating AttentionCoordinator.extractAttentionWeights and the four affected coordination entry points, then run the supplied hierarchicalCoordination reproduction. Add a regression test using differing values that verifies consensus remains within the input range and weights form a valid distribution. Done means the reported extrapolation and divide-by-sum failure are covered across the relevant paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
ai
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.