fix(eve): scope forget, persist capture idempotency, and fail closed
Review findings: delete was not partitioned by scope.key, capture replay after restart could double-write, and Mem0 errors were swallowed as empty recall. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
1d83bb0835
commit
10d6717068
@@ -1,14 +1,23 @@
|
||||
import { completedTurnMessages, type ConversationMessage } from "./messages.js";
|
||||
import type { MemoryStore } from "./store.js";
|
||||
import type { MemoryStore, StoreMetadata } from "./store.js";
|
||||
|
||||
export async function captureCompletedTurn(input: {
|
||||
store: MemoryStore;
|
||||
scopeKey: string;
|
||||
operationId: string;
|
||||
infer: boolean;
|
||||
metadata: Readonly<Record<string, unknown>>;
|
||||
metadata: StoreMetadata;
|
||||
messages: readonly ConversationMessage[];
|
||||
turnInput: readonly ConversationMessage[];
|
||||
}): Promise<boolean> {
|
||||
const existing = await input.store.listByMetadata({
|
||||
userId: input.scopeKey,
|
||||
metadata: { operation_id: input.operationId },
|
||||
});
|
||||
if (existing.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messages = completedTurnMessages({
|
||||
messages: input.messages,
|
||||
turnInput: input.turnInput,
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
export function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : "Unknown Mem0 error";
|
||||
}
|
||||
|
||||
export function isSetupError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
const text = `${error.name} ${error.message}`.toLowerCase();
|
||||
return (
|
||||
text.includes("api key") ||
|
||||
text.includes("cannot be empty") ||
|
||||
text.includes("authentication") ||
|
||||
text.includes("unauthorized") ||
|
||||
text.includes("401") ||
|
||||
text.includes("403")
|
||||
);
|
||||
}
|
||||
|
||||
export function isNotFoundError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
const text = `${error.name} ${error.message}`.toLowerCase();
|
||||
return text.includes("not found") || text.includes("404");
|
||||
}
|
||||
|
||||
export function logProviderError(
|
||||
scope: string,
|
||||
error: unknown,
|
||||
extra: Readonly<Record<string, unknown>>,
|
||||
): void {
|
||||
console.error(`[@mem0/eve] ${scope}`, {
|
||||
error,
|
||||
name: error instanceof Error ? error.name : undefined,
|
||||
message: errorMessage(error),
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const DEFAULT_LIMIT = 256;
|
||||
|
||||
// In-process fast path only. Durable replay after restart is handled by
|
||||
// listing memories with metadata.operation_id before add.
|
||||
export function createIdempotencyGate(limit = DEFAULT_LIMIT) {
|
||||
const seen = new Map<string, Promise<void>>();
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export { mem0Provider, mem0Provider as default } from "./provider.js";
|
||||
export type { Mem0ApiKey, Mem0ProviderOptions } from "./options.js";
|
||||
export type {
|
||||
Mem0ApiKey,
|
||||
Mem0InjectedProviderOptions,
|
||||
Mem0ProviderOptions,
|
||||
Mem0RemoteProviderOptions,
|
||||
} from "./options.js";
|
||||
export type { MemoryStore, MemoryMessage, SearchHit } from "./store.js";
|
||||
|
||||
@@ -5,27 +5,29 @@ export interface ConversationMessage {
|
||||
readonly content: unknown;
|
||||
}
|
||||
|
||||
function textFromPart(part: unknown): string {
|
||||
if (typeof part === "string") {
|
||||
return part;
|
||||
}
|
||||
if (part && typeof part === "object" && "text" in part) {
|
||||
const text = (part as { text: unknown }).text;
|
||||
return typeof text === "string" ? text : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function extractText(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return content.trim();
|
||||
}
|
||||
if (content && typeof content === "object" && !Array.isArray(content)) {
|
||||
return textFromPart(content).trim();
|
||||
}
|
||||
if (!Array.isArray(content)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") {
|
||||
return part;
|
||||
}
|
||||
if (part && typeof part === "object" && "text" in part) {
|
||||
const text = (part as { text: unknown }).text;
|
||||
return typeof text === "string" ? text : "";
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join("\n")
|
||||
.trim();
|
||||
return content.map(textFromPart).join("\n").trim();
|
||||
}
|
||||
|
||||
export function lastUserText(messages: readonly ConversationMessage[]): string {
|
||||
@@ -70,6 +72,7 @@ export function completedTurnMessages(input: {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Pair this turn's users with the latest assistant in the settled history.
|
||||
const history = conversationMessages(input.messages);
|
||||
const lastAssistant = [...history]
|
||||
.reverse()
|
||||
|
||||
@@ -2,9 +2,7 @@ import type { MemoryStore } from "./store.js";
|
||||
|
||||
export type Mem0ApiKey = string | (() => string | Promise<string>);
|
||||
|
||||
export interface Mem0ProviderOptions {
|
||||
readonly apiKey: Mem0ApiKey;
|
||||
readonly host?: string;
|
||||
export interface Mem0SharedProviderOptions {
|
||||
readonly topK?: number;
|
||||
readonly threshold?: number;
|
||||
readonly rerank?: boolean;
|
||||
@@ -16,11 +14,23 @@ export interface Mem0ProviderOptions {
|
||||
readonly enabled?: boolean;
|
||||
};
|
||||
readonly metadata?: Readonly<Record<string, string | number | boolean>>;
|
||||
readonly store?: MemoryStore;
|
||||
}
|
||||
|
||||
export interface ResolvedMem0ProviderOptions {
|
||||
export interface Mem0RemoteProviderOptions extends Mem0SharedProviderOptions {
|
||||
readonly apiKey: Mem0ApiKey;
|
||||
readonly host?: string;
|
||||
}
|
||||
|
||||
export interface Mem0InjectedProviderOptions extends Mem0SharedProviderOptions {
|
||||
readonly store: MemoryStore;
|
||||
readonly apiKey?: Mem0ApiKey;
|
||||
readonly host?: string;
|
||||
}
|
||||
|
||||
export type Mem0ProviderOptions = Mem0RemoteProviderOptions | Mem0InjectedProviderOptions;
|
||||
|
||||
export interface ResolvedMem0ProviderOptions {
|
||||
readonly apiKey?: Mem0ApiKey;
|
||||
readonly host: string;
|
||||
readonly topK: number;
|
||||
readonly threshold: number;
|
||||
@@ -36,13 +46,38 @@ export interface ResolvedMem0ProviderOptions {
|
||||
readonly store?: MemoryStore;
|
||||
}
|
||||
|
||||
function validateApiKey(apiKey: Mem0ApiKey): void {
|
||||
if (typeof apiKey === "string" && apiKey.trim().length === 0) {
|
||||
throw new Error("Mem0 API key cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
function resolveHost(host: string | undefined): string {
|
||||
const trimmed = host?.trim() ?? "";
|
||||
if (trimmed.length === 0) {
|
||||
return "https://api.mem0.ai";
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
throw new Error("host must be an http or https URL");
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
throw new Error("host must be an http or https URL");
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function resolveOptions(options: Mem0ProviderOptions): ResolvedMem0ProviderOptions {
|
||||
const store = "store" in options ? options.store : undefined;
|
||||
const apiKey = options.apiKey;
|
||||
if (typeof apiKey === "string") {
|
||||
const trimmed = apiKey.trim();
|
||||
if (trimmed.length === 0) {
|
||||
throw new Error("Mem0 API key cannot be empty");
|
||||
}
|
||||
if (!store && apiKey === undefined) {
|
||||
throw new Error("apiKey is required unless a store is provided");
|
||||
}
|
||||
if (apiKey !== undefined) {
|
||||
validateApiKey(apiKey);
|
||||
}
|
||||
|
||||
const topK = options.topK ?? 5;
|
||||
@@ -56,8 +91,8 @@ export function resolveOptions(options: Mem0ProviderOptions): ResolvedMem0Provid
|
||||
}
|
||||
|
||||
return {
|
||||
apiKey,
|
||||
host: options.host ?? "https://api.mem0.ai",
|
||||
...(apiKey !== undefined ? { apiKey } : {}),
|
||||
host: resolveHost(options.host),
|
||||
topK,
|
||||
threshold,
|
||||
rerank: options.rerank ?? false,
|
||||
@@ -69,6 +104,6 @@ export function resolveOptions(options: Mem0ProviderOptions): ResolvedMem0Provid
|
||||
enabled: options.capture?.enabled ?? true,
|
||||
},
|
||||
metadata: options.metadata ?? {},
|
||||
...(options.store ? { store: options.store } : {}),
|
||||
...(store ? { store } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { defineMemoryProvider, type MemoryProvider } from "eve/memory";
|
||||
import { captureCompletedTurn } from "./capture.js";
|
||||
import { errorMessage } from "./errors.js";
|
||||
import { isSetupError, logProviderError } from "./errors.js";
|
||||
import { createIdempotencyGate } from "./idempotency.js";
|
||||
import { lastUserText, type ConversationMessage } from "./messages.js";
|
||||
import { resolveOptions, type Mem0ProviderOptions } from "./options.js";
|
||||
import { recallMemories } from "./recall.js";
|
||||
import { createMem0Store, type MemoryStore } from "./store.js";
|
||||
import { createLazyStore, createMem0Store, type MemoryStore } from "./store.js";
|
||||
import { createMem0Tools } from "./tools.js";
|
||||
|
||||
function asConversation(messages: readonly unknown[]): ConversationMessage[] {
|
||||
@@ -14,7 +14,9 @@ function asConversation(messages: readonly unknown[]): ConversationMessage[] {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
"role" in message && typeof (message as { role: unknown }).role === "string"
|
||||
"role" in message &&
|
||||
typeof (message as { role: unknown }).role === "string" &&
|
||||
"content" in message
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -22,17 +24,21 @@ function asConversation(messages: readonly unknown[]): ConversationMessage[] {
|
||||
export function mem0Provider(options: Mem0ProviderOptions): MemoryProvider {
|
||||
const config = resolveOptions(options);
|
||||
const once = createIdempotencyGate();
|
||||
let storePromise: Promise<MemoryStore> | undefined;
|
||||
const loadRemoteStore = createLazyStore(async () => {
|
||||
if (!config.apiKey) {
|
||||
throw new Error("Mem0 API key cannot be empty");
|
||||
}
|
||||
return createMem0Store({
|
||||
apiKey: config.apiKey,
|
||||
host: config.host,
|
||||
});
|
||||
});
|
||||
|
||||
const getStore = async (): Promise<MemoryStore> => {
|
||||
if (config.store) {
|
||||
return config.store;
|
||||
}
|
||||
storePromise ??= createMem0Store({
|
||||
apiKey: config.apiKey,
|
||||
host: config.host,
|
||||
});
|
||||
return storePromise;
|
||||
return loadRemoteStore();
|
||||
};
|
||||
|
||||
const recall = async (context: {
|
||||
@@ -47,7 +53,13 @@ export function mem0Provider(options: Mem0ProviderOptions): MemoryProvider {
|
||||
}
|
||||
|
||||
const querySource = context.turn?.input ?? context.messages;
|
||||
const query = lastUserText(asConversation(querySource));
|
||||
const conversation = asConversation(querySource);
|
||||
if (querySource.length > 0 && conversation.length === 0) {
|
||||
console.error("[@mem0/eve] dropped all recall messages; no searchable user text", {
|
||||
sessionId: context.session.id,
|
||||
});
|
||||
}
|
||||
const query = lastUserText(conversation);
|
||||
|
||||
try {
|
||||
const store = await getStore();
|
||||
@@ -60,14 +72,13 @@ export function mem0Provider(options: Mem0ProviderOptions): MemoryProvider {
|
||||
rerank: config.rerank,
|
||||
});
|
||||
} catch (error) {
|
||||
if (context.abortSignal.aborted) {
|
||||
if (context.abortSignal.aborted || isSetupError(error)) {
|
||||
throw error;
|
||||
}
|
||||
console.error("[@mem0/eve] recall failed", {
|
||||
error: errorMessage(error),
|
||||
logProviderError("recall failed", error, {
|
||||
sessionId: context.session.id,
|
||||
});
|
||||
return null;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,41 +91,36 @@ export function mem0Provider(options: Mem0ProviderOptions): MemoryProvider {
|
||||
? {
|
||||
capture: {
|
||||
async "turn.completed"(context) {
|
||||
try {
|
||||
const store = await getStore();
|
||||
await once(context.operationId, async () => {
|
||||
await captureCompletedTurn({
|
||||
store,
|
||||
scopeKey: context.memory.scope.key,
|
||||
infer: config.infer,
|
||||
metadata: {
|
||||
...config.metadata,
|
||||
source: "eve",
|
||||
operation_id: context.operationId,
|
||||
session_id: context.session.id,
|
||||
slot: context.memory.slot,
|
||||
},
|
||||
messages: asConversation(context.messages),
|
||||
turnInput: asConversation(context.turn.input),
|
||||
});
|
||||
const store = await getStore();
|
||||
await once(context.operationId, async () => {
|
||||
await captureCompletedTurn({
|
||||
store,
|
||||
scopeKey: context.memory.scope.key,
|
||||
operationId: context.operationId,
|
||||
infer: config.infer,
|
||||
metadata: {
|
||||
...config.metadata,
|
||||
source: "eve",
|
||||
operation_id: context.operationId,
|
||||
session_id: context.session.id,
|
||||
slot: context.memory.slot,
|
||||
},
|
||||
messages: asConversation(context.messages),
|
||||
turnInput: asConversation(context.turn.input),
|
||||
});
|
||||
} catch (error) {
|
||||
if (context.abortSignal.aborted) {
|
||||
throw error;
|
||||
}
|
||||
console.error("[@mem0/eve] capture failed", {
|
||||
error: errorMessage(error),
|
||||
sessionId: context.session.id,
|
||||
turnId: context.turn.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
async tools(context) {
|
||||
const store = await getStore();
|
||||
return createMem0Tools(store, context.memory.scope.key);
|
||||
return createMem0Tools(store, context.memory.scope.key, {
|
||||
topK: config.topK,
|
||||
threshold: config.threshold,
|
||||
rerank: config.rerank,
|
||||
infer: config.infer,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ export interface RecallMessage {
|
||||
|
||||
export function formatRecallMessages(hits: readonly SearchHit[]): RecallMessage[] {
|
||||
return hits.flatMap((hit) => {
|
||||
const id = hit.id.trim();
|
||||
const content = hit.memory.trim();
|
||||
return content.length > 0 ? [{ id: hit.id, content }] : [];
|
||||
return id.length > 0 && content.length > 0 ? [{ id, content }] : [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@ export async function recallMemories(input: {
|
||||
}
|
||||
|
||||
const { results } = await input.store.search(query, {
|
||||
filters: { user_id: input.scopeKey },
|
||||
userId: input.scopeKey,
|
||||
topK: input.topK,
|
||||
threshold: input.threshold,
|
||||
rerank: input.rerank,
|
||||
|
||||
+126
-17
@@ -1,4 +1,5 @@
|
||||
import type { Mem0ApiKey } from "./options.js";
|
||||
import { isNotFoundError } from "./errors.js";
|
||||
|
||||
export type MemoryRole = "user" | "assistant";
|
||||
|
||||
@@ -12,25 +13,94 @@ export interface SearchHit {
|
||||
readonly memory: string;
|
||||
}
|
||||
|
||||
export interface AddMemoryInput {
|
||||
export interface MemoryRecord {
|
||||
readonly id: string;
|
||||
readonly userId: string;
|
||||
readonly infer: boolean;
|
||||
readonly metadata: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export type StoreMetadata = Readonly<Record<string, string | number | boolean>>;
|
||||
|
||||
export interface AddMemoryInput {
|
||||
readonly userId: string;
|
||||
readonly infer: boolean;
|
||||
readonly metadata: StoreMetadata;
|
||||
}
|
||||
|
||||
export interface SearchMemoryInput {
|
||||
readonly filters: {
|
||||
readonly user_id: string;
|
||||
};
|
||||
readonly userId: string;
|
||||
readonly topK: number;
|
||||
readonly threshold: number;
|
||||
readonly rerank: boolean;
|
||||
}
|
||||
|
||||
export interface MemoryStore {
|
||||
add(messages: readonly MemoryMessage[], input: AddMemoryInput): Promise<unknown>;
|
||||
add(messages: readonly MemoryMessage[], input: AddMemoryInput): Promise<void>;
|
||||
search(query: string, input: SearchMemoryInput): Promise<{ results: readonly SearchHit[] }>;
|
||||
delete(memoryId: string): Promise<unknown>;
|
||||
get(memoryId: string): Promise<MemoryRecord | null>;
|
||||
listByMetadata(input: {
|
||||
readonly userId: string;
|
||||
readonly metadata: Readonly<Record<string, string>>;
|
||||
}): Promise<readonly MemoryRecord[]>;
|
||||
delete(memoryId: string, userId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export function parseSearchHit(item: unknown): SearchHit | null {
|
||||
if (!item || typeof item !== "object") {
|
||||
return null;
|
||||
}
|
||||
const record = item as Record<string, unknown>;
|
||||
const nested =
|
||||
record.data && typeof record.data === "object"
|
||||
? (record.data as Record<string, unknown>)
|
||||
: undefined;
|
||||
const idValue = record.id ?? record.memory_id ?? nested?.id;
|
||||
const id =
|
||||
typeof idValue === "string"
|
||||
? idValue.trim()
|
||||
: typeof idValue === "number"
|
||||
? String(idValue)
|
||||
: "";
|
||||
const memoryValue =
|
||||
record.memory ?? record.text ?? nested?.memory ?? nested?.text;
|
||||
const memory = typeof memoryValue === "string" ? memoryValue : "";
|
||||
if (id.length === 0 || memory.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
return { id, memory };
|
||||
}
|
||||
|
||||
export function parseMemoryRecord(item: unknown): MemoryRecord | null {
|
||||
if (!item || typeof item !== "object") {
|
||||
return null;
|
||||
}
|
||||
const record = item as Record<string, unknown>;
|
||||
const idValue = record.id ?? record.memory_id;
|
||||
const id = typeof idValue === "string" ? idValue.trim() : "";
|
||||
const userIdValue = record.userId ?? record.user_id;
|
||||
const userId = typeof userIdValue === "string" ? userIdValue : "";
|
||||
if (id.length === 0 || userId.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const metadata =
|
||||
record.metadata && typeof record.metadata === "object"
|
||||
? (record.metadata as Record<string, unknown>)
|
||||
: {};
|
||||
return { id, userId, metadata };
|
||||
}
|
||||
|
||||
function asItemList(response: unknown, operation: string): unknown[] {
|
||||
if (Array.isArray(response)) {
|
||||
return response;
|
||||
}
|
||||
if (response && typeof response === "object") {
|
||||
const record = response as Record<string, unknown>;
|
||||
const raw = record.results ?? record.memories;
|
||||
if (Array.isArray(raw)) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
throw new Error(`Mem0 ${operation} returned an unexpected response shape`);
|
||||
}
|
||||
|
||||
export async function resolveApiKey(apiKey: Mem0ApiKey): Promise<string> {
|
||||
@@ -42,6 +112,20 @@ export async function resolveApiKey(apiKey: Mem0ApiKey): Promise<string> {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function createLazyStore(factory: () => Promise<MemoryStore>) {
|
||||
let pending: Promise<MemoryStore> | undefined;
|
||||
|
||||
return async (): Promise<MemoryStore> => {
|
||||
if (!pending) {
|
||||
pending = factory().catch((error: unknown) => {
|
||||
pending = undefined;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return pending;
|
||||
};
|
||||
}
|
||||
|
||||
export async function createMem0Store(input: {
|
||||
apiKey: Mem0ApiKey;
|
||||
host: string;
|
||||
@@ -52,9 +136,9 @@ export async function createMem0Store(input: {
|
||||
host: input.host,
|
||||
});
|
||||
|
||||
return {
|
||||
const store: MemoryStore = {
|
||||
async add(messages, options) {
|
||||
return client.add([...messages], {
|
||||
await client.add([...messages], {
|
||||
userId: options.userId,
|
||||
infer: options.infer,
|
||||
metadata: { ...options.metadata },
|
||||
@@ -62,22 +146,47 @@ export async function createMem0Store(input: {
|
||||
},
|
||||
async search(query, options) {
|
||||
const response = await client.search(query, {
|
||||
filters: options.filters,
|
||||
filters: { user_id: options.userId },
|
||||
topK: options.topK,
|
||||
threshold: options.threshold,
|
||||
rerank: options.rerank,
|
||||
});
|
||||
const results = Array.isArray(response?.results) ? response.results : [];
|
||||
return {
|
||||
results: results.flatMap((item) => {
|
||||
const id = typeof item.id === "string" ? item.id : "";
|
||||
const memory = typeof item.memory === "string" ? item.memory : "";
|
||||
return id && memory ? [{ id, memory }] : [];
|
||||
results: asItemList(response, "search").flatMap((item) => {
|
||||
const hit = parseSearchHit(item);
|
||||
return hit ? [hit] : [];
|
||||
}),
|
||||
};
|
||||
},
|
||||
async delete(memoryId) {
|
||||
return client.delete(memoryId);
|
||||
async get(memoryId) {
|
||||
try {
|
||||
return parseMemoryRecord(await client.get(memoryId));
|
||||
} catch (error) {
|
||||
if (isNotFoundError(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async listByMetadata({ userId, metadata }) {
|
||||
const response = await client.getAll({
|
||||
filters: {
|
||||
AND: [{ user_id: userId }, { metadata }],
|
||||
},
|
||||
pageSize: 5,
|
||||
});
|
||||
return asItemList(response, "getAll").flatMap((item) => {
|
||||
const record = parseMemoryRecord(item);
|
||||
return record && record.userId === userId ? [record] : [];
|
||||
});
|
||||
},
|
||||
async delete(memoryId, userId) {
|
||||
const record = await store.get(memoryId);
|
||||
if (!record || record.userId !== userId) {
|
||||
throw new Error("Memory not found");
|
||||
}
|
||||
await client.delete(memoryId);
|
||||
},
|
||||
};
|
||||
return store;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,18 @@ import { defineTool } from "eve/tools";
|
||||
import { z } from "zod";
|
||||
import type { MemoryStore } from "./store.js";
|
||||
|
||||
export function createMem0Tools(store: MemoryStore, scopeKey: string) {
|
||||
export interface Mem0ToolOptions {
|
||||
readonly topK: number;
|
||||
readonly threshold: number;
|
||||
readonly rerank: boolean;
|
||||
readonly infer: boolean;
|
||||
}
|
||||
|
||||
export function createMem0Tools(
|
||||
store: MemoryStore,
|
||||
scopeKey: string,
|
||||
options: Mem0ToolOptions,
|
||||
) {
|
||||
return {
|
||||
search: defineTool({
|
||||
description: "Search long-term memories for the current caller.",
|
||||
@@ -11,10 +22,10 @@ export function createMem0Tools(store: MemoryStore, scopeKey: string) {
|
||||
}),
|
||||
async execute({ query }) {
|
||||
const { results } = await store.search(query, {
|
||||
filters: { user_id: scopeKey },
|
||||
topK: 8,
|
||||
threshold: 0.1,
|
||||
rerank: false,
|
||||
userId: scopeKey,
|
||||
topK: options.topK,
|
||||
threshold: options.threshold,
|
||||
rerank: options.rerank,
|
||||
});
|
||||
return {
|
||||
memories: results.map((hit) => ({
|
||||
@@ -33,7 +44,7 @@ export function createMem0Tools(store: MemoryStore, scopeKey: string) {
|
||||
async execute({ text }) {
|
||||
await store.add([{ role: "user", content: text }], {
|
||||
userId: scopeKey,
|
||||
infer: true,
|
||||
infer: options.infer,
|
||||
metadata: { source: "eve-tool" },
|
||||
});
|
||||
return { saved: true };
|
||||
@@ -45,7 +56,7 @@ export function createMem0Tools(store: MemoryStore, scopeKey: string) {
|
||||
id: z.string().min(1),
|
||||
}),
|
||||
async execute({ id }) {
|
||||
await store.delete(id);
|
||||
await store.delete(id, scopeKey);
|
||||
return { deleted: true };
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user