Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

316 lines
8.2 KiB
TypeScript

/**
* `EphemeralHNSW`: an in-memory, cosine-distance HNSW (Hierarchical Navigable
* Small World) vector index implementing `IVectorStorage`. Nodes are assigned
* a random level on insert; search descends from the entry point through
* upper layers before doing a beam search (`efSearch`) on layer 0. All state
* lives in the `nodes` map — nothing is persisted, and `clear()`/losing the
* process discards the index entirely.
*/
import type { IVectorStorage, VectorSearchResult } from "./types";
interface HNSWNode {
id: string;
vector: number[];
level: number;
neighbors: Map<number, Set<string>>;
}
interface HNSWConfig {
M: number;
efConstruction: number;
efSearch: number;
mL: number;
}
function cosineDistance(a: number[], b: number[]): number {
if (a.length !== b.length) {
throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);
}
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
const aValue = a[i] ?? 0;
const bValue = b[i] ?? 0;
dotProduct += aValue * bValue;
normA += aValue * aValue;
normB += bValue * bValue;
}
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
if (magnitude === 0) return 1;
return 1 - dotProduct / magnitude;
}
export class EphemeralHNSW implements IVectorStorage {
private nodes: Map<string, HNSWNode> = new Map();
private entryPoint: string | null = null;
private maxLevel = 0;
private dimension = 0;
private config: HNSWConfig;
constructor() {
this.config = {
M: 16,
efConstruction: 200,
efSearch: 50,
mL: 1 / Math.log(16),
};
}
async init(dimension: number): Promise<void> {
this.dimension = dimension;
}
private getRandomLevel(): number {
let level = 0;
while (Math.random() < Math.exp(-level * this.config.mL) && level < 16) {
level++;
}
return level;
}
async add(id: string, vector: number[]): Promise<void> {
if (vector.length !== this.dimension) {
throw new Error(
`Vector dimension mismatch: expected ${this.dimension}, got ${vector.length}`
);
}
const existing = this.nodes.get(id);
if (existing) {
existing.vector = vector;
return;
}
const level = this.getRandomLevel();
const newNode: HNSWNode = {
id,
vector,
level,
neighbors: new Map(),
};
for (let l = 0; l <= level; l++) {
newNode.neighbors.set(l, new Set());
}
if (this.entryPoint === null) {
this.entryPoint = id;
this.maxLevel = level;
this.nodes.set(id, newNode);
return;
}
let currentNode = this.entryPoint;
for (let l = this.maxLevel; l > level; l--) {
currentNode = this.searchLayer(vector, currentNode, 1, l)[0]?.id ?? currentNode;
}
for (let l = Math.min(level, this.maxLevel); l >= 0; l--) {
const neighbors = this.searchLayer(vector, currentNode, this.config.efConstruction, l);
const M = this.config.M;
const selectedNeighbors = neighbors.slice(0, M);
for (const neighbor of selectedNeighbors) {
const neighborSet = newNode.neighbors.get(l);
if (neighborSet) {
neighborSet.add(neighbor.id);
}
const neighborNode = this.nodes.get(neighbor.id);
if (neighborNode) {
let neighborSet = neighborNode.neighbors.get(l);
if (!neighborSet) {
neighborSet = new Set();
neighborNode.neighbors.set(l, neighborSet);
}
neighborSet.add(id);
if (neighborSet.size > M) {
const toKeep = this.selectBestNeighbors(neighborNode.vector, neighborSet, M);
neighborNode.neighbors.set(l, new Set(toKeep.map((n) => n.id)));
}
}
}
if (neighbors.length > 0) {
const closestNeighbor = neighbors[0];
if (closestNeighbor) {
currentNode = closestNeighbor.id;
}
}
}
this.nodes.set(id, newNode);
if (level > this.maxLevel) {
this.maxLevel = level;
this.entryPoint = id;
}
}
private searchLayer(
query: number[],
entryId: string,
ef: number,
level: number
): Array<{ id: string; distance: number }> {
const visited = new Set<string>([entryId]);
const entryNode = this.nodes.get(entryId);
if (!entryNode) return [];
const entryDist = cosineDistance(query, entryNode.vector);
const candidates: Array<{ id: string; distance: number }> = [
{ id: entryId, distance: entryDist },
];
const results: Array<{ id: string; distance: number }> = [{ id: entryId, distance: entryDist }];
while (candidates.length > 0) {
candidates.sort((a, b) => a.distance - b.distance);
const current = candidates.shift();
if (!current) break;
results.sort((a, b) => b.distance - a.distance);
const furthestResult = results[0];
if (!furthestResult) break;
if (current.distance > furthestResult.distance) {
break;
}
const currentNode = this.nodes.get(current.id);
if (!currentNode) continue;
const neighbors = currentNode.neighbors.get(level);
if (!neighbors) continue;
for (const neighborId of neighbors) {
if (visited.has(neighborId)) continue;
visited.add(neighborId);
const neighborNode = this.nodes.get(neighborId);
if (!neighborNode) continue;
const dist = cosineDistance(query, neighborNode.vector);
if (results.length < ef || dist < furthestResult.distance) {
candidates.push({ id: neighborId, distance: dist });
results.push({ id: neighborId, distance: dist });
if (results.length > ef) {
results.sort((a, b) => b.distance - a.distance);
results.pop();
}
}
}
}
results.sort((a, b) => a.distance - b.distance);
return results;
}
private selectBestNeighbors(
nodeVector: number[],
neighborIds: Set<string>,
M: number
): Array<{ id: string; distance: number }> {
const neighbors: Array<{ id: string; distance: number }> = [];
for (const id of neighborIds) {
const node = this.nodes.get(id);
if (node) {
neighbors.push({
id,
distance: cosineDistance(nodeVector, node.vector),
});
}
}
neighbors.sort((a, b) => a.distance - b.distance);
return neighbors.slice(0, M);
}
async remove(id: string): Promise<void> {
const node = this.nodes.get(id);
if (!node) return;
for (const [level, neighbors] of node.neighbors) {
for (const neighborId of neighbors) {
const neighborNode = this.nodes.get(neighborId);
if (neighborNode) {
neighborNode.neighbors.get(level)?.delete(id);
}
}
}
this.nodes.delete(id);
if (this.entryPoint === id) {
if (this.nodes.size === 0) {
this.entryPoint = null;
this.maxLevel = 0;
} else {
let maxLevel = 0;
let newEntry: string | null = null;
for (const [nodeId, n] of this.nodes) {
if (n.level >= maxLevel) {
maxLevel = n.level;
newEntry = nodeId;
}
}
this.entryPoint = newEntry;
this.maxLevel = maxLevel;
}
}
}
async search(query: number[], k: number, threshold = 0.5): Promise<VectorSearchResult[]> {
if (this.entryPoint === null || this.nodes.size === 0) {
return [];
}
if (query.length !== this.dimension) {
throw new Error(`Query dimension mismatch: expected ${this.dimension}, got ${query.length}`);
}
let currentNode = this.entryPoint;
for (let l = this.maxLevel; l > 0; l--) {
const closest = this.searchLayer(query, currentNode, 1, l);
const closestNode = closest[0];
if (closestNode) {
currentNode = closestNode.id;
}
}
const results = this.searchLayer(query, currentNode, Math.max(k, this.config.efSearch), 0);
return results
.slice(0, k)
.filter((r) => 1 - r.distance >= threshold)
.map((r) => ({
id: r.id,
distance: r.distance,
similarity: 1 - r.distance,
}));
}
async clear(): Promise<void> {
this.nodes.clear();
this.entryPoint = null;
this.maxLevel = 0;
}
size(): number {
return this.nodes.size;
}
}