Merge Memory V2 runtime facade
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
function normalizeUrl(value) {
|
||||
const raw = String(value ?? '').trim();
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const url = new URL(raw);
|
||||
if (!['redis:', 'rediss:'].includes(url.protocol)) {
|
||||
throw new Error(`Unsupported protocol: ${url.protocol}`);
|
||||
}
|
||||
return raw;
|
||||
} catch (err) {
|
||||
throw new Error(`Invalid MEMORY_REDIS_STREAMS_URL: ${err instanceof Error ? err.message : err}`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStream(value) {
|
||||
const raw = String(value ?? 'memind:memory-events').trim();
|
||||
if (!/^[a-zA-Z0-9:_-]+$/.test(raw)) {
|
||||
throw new Error(`Invalid MEMORY_REDIS_STREAMS_STREAM: ${raw}`);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function flattenFields(input = {}) {
|
||||
return {
|
||||
user_id: String(input.userId ?? ''),
|
||||
session_id: String(input.sessionId ?? ''),
|
||||
event_type: String(input.eventType ?? 'memory.write'),
|
||||
messages_json: JSON.stringify(Array.isArray(input.messages) ? input.messages : []),
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createRedisStreamsClient({
|
||||
url,
|
||||
stream = 'memind:memory-events',
|
||||
importRedis = (specifier) => import(specifier),
|
||||
} = {}) {
|
||||
const resolvedUrl = normalizeUrl(url);
|
||||
const resolvedStream = normalizeStream(stream);
|
||||
if (!resolvedUrl) throw new Error('createRedisStreamsClient requires MEMORY_REDIS_STREAMS_URL');
|
||||
const imported = await importRedis('redis');
|
||||
const createClient = imported?.createClient ?? imported?.default?.createClient;
|
||||
if (typeof createClient !== 'function') throw new Error('redis module does not export createClient');
|
||||
const client = createClient({ url: resolvedUrl });
|
||||
let connected = false;
|
||||
|
||||
async function ensureConnected() {
|
||||
if (connected) return;
|
||||
await client.connect();
|
||||
connected = true;
|
||||
}
|
||||
|
||||
return {
|
||||
async write(input = {}) {
|
||||
await ensureConnected();
|
||||
await client.xAdd(resolvedStream, '*', flattenFields(input));
|
||||
return { saved: 1, analyzed: 0, memories: 0 };
|
||||
},
|
||||
async close() {
|
||||
if (!connected) return;
|
||||
connected = false;
|
||||
await client.quit?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createRedisStreamsMemoryBackend({
|
||||
enabled = false,
|
||||
url = null,
|
||||
stream = 'memind:memory-events',
|
||||
client = null,
|
||||
unavailableReason = null,
|
||||
} = {}) {
|
||||
const resolvedUrl = normalizeUrl(url);
|
||||
const resolvedStream = normalizeStream(stream);
|
||||
const configured = Boolean(enabled && resolvedUrl);
|
||||
const hasClient = Boolean(client?.write);
|
||||
const wired = Boolean(configured && hasClient);
|
||||
const reason = unavailableReason
|
||||
?? (enabled
|
||||
? (!resolvedUrl ? 'url_not_configured' : 'client_not_configured')
|
||||
: 'not_configured');
|
||||
|
||||
return {
|
||||
name: 'redis-streams',
|
||||
category: 'behavior',
|
||||
role: 'event-tracking',
|
||||
flag: 'MEMORY_REDIS_STREAMS_ENABLED',
|
||||
unavailableReason: reason,
|
||||
url: resolvedUrl,
|
||||
stream: resolvedStream,
|
||||
|
||||
isAvailable() {
|
||||
return Boolean(wired);
|
||||
},
|
||||
|
||||
getUnavailableReason() {
|
||||
return this.isAvailable() ? null : reason;
|
||||
},
|
||||
|
||||
async write(input = {}) {
|
||||
if (!this.isAvailable()) return { saved: 0, analyzed: 0, memories: 0 };
|
||||
const result = await client.write({ ...input, url: resolvedUrl, stream: resolvedStream });
|
||||
return {
|
||||
saved: Number(result?.saved ?? 0),
|
||||
analyzed: Number(result?.analyzed ?? 0),
|
||||
memories: Number(result?.memories ?? 0),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user