Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-vector-search-results.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@voltagent/core": patch
---

Copy embeddings and metadata in in-memory vector search results so caller mutations do not corrupt stored memories or change subsequent user-filtered searches.
44 changes: 44 additions & 0 deletions packages/core/src/memory/adapters/vector/in-memory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { InMemoryVectorAdapter } from "./in-memory";

describe("InMemoryVectorAdapter search result isolation", () => {
it("does not let a returned embedding corrupt future similarity searches", async () => {
const adapter = new InMemoryVectorAdapter();
await adapter.store("matching", [1, 0], { userId: "alice" });
await adapter.store("other", [0, 1], { userId: "alice" });

const [result] = await adapter.search([1, 0]);
result.vector[0] = 0;
result.vector[1] = 1;

expect((await adapter.get("matching"))?.vector).toEqual([1, 0]);
const [nextResult] = await adapter.search([1, 0]);
expect(nextResult.id).toBe("matching");
expect(nextResult.score).toBe(1);
});

it("does not let returned metadata change user-scoped retrieval", async () => {
const adapter = new InMemoryVectorAdapter();
await adapter.store("memory", [1, 0], { userId: "alice" });

const [result] = await adapter.search([1, 0], { filter: { userId: "alice" } });
if (!result.metadata) throw new Error("Expected stored metadata");
result.metadata.userId = "bob";

expect(await adapter.search([1, 0], { filter: { userId: "bob" } })).toEqual([]);
expect(await adapter.search([1, 0], { filter: { userId: "alice" } })).toHaveLength(1);
expect((await adapter.get("memory"))?.metadata).toEqual({ userId: "alice" });
});

it("supports results without metadata and keeps stored input isolated", async () => {
const adapter = new InMemoryVectorAdapter();
const vector = [1, 0];
await adapter.store("memory", vector);
vector[0] = 0;

const [result] = await adapter.search([1, 0]);
expect(result.vector).toEqual([1, 0]);
expect(result.metadata).toBeUndefined();
expect(result.score).toBe(1);
});
});
2 changes: 2 additions & 0 deletions packages/core/src/memory/adapters/vector/in-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export class InMemoryVectorAdapter implements VectorAdapter {
if (score >= threshold) {
results.push({
...item,
vector: [...item.vector],
metadata: item.metadata ? { ...item.metadata } : undefined,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
score,
distance: 1 - similarity, // Convert to distance metric
});
Expand Down