Merge Memory V2 runtime facade
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
function normalizeLettaId(value, envName) {
|
||||
const raw = String(value ?? '').trim();
|
||||
if (!raw) return null;
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(raw)) {
|
||||
throw new Error(`Invalid ${envName}: ${raw}`);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function normalizeMemory(item) {
|
||||
if (typeof item === 'string') {
|
||||
const text = item.trim();
|
||||
return text ? { label: 'lifecycle', text } : null;
|
||||
}
|
||||
const text = String(
|
||||
item?.text ?? item?.content ?? item?.memory_text ?? item?.memoryText ?? '',
|
||||
).trim();
|
||||
if (!text) return null;
|
||||
return {
|
||||
id: item?.id == null ? null : String(item.id),
|
||||
label: item?.label ?? item?.type ?? 'lifecycle',
|
||||
text,
|
||||
createdAt: item?.createdAt ?? item?.created_at ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeResolvePayload(payload = {}) {
|
||||
const memories = Array.isArray(payload?.memories)
|
||||
? payload.memories.map((item) => normalizeMemory(item)).filter(Boolean)
|
||||
: [];
|
||||
return {
|
||||
memories,
|
||||
semanticMemories: Array.isArray(payload?.semanticMemories)
|
||||
? payload.semanticMemories
|
||||
: memories.map((memory) => memory.text),
|
||||
behaviorSummary: payload?.behaviorSummary ?? null,
|
||||
activeGoals: Array.isArray(payload?.activeGoals) ? payload.activeGoals : [],
|
||||
profile: payload?.profile ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function timeoutSignal(timeoutMs) {
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return null;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
return {
|
||||
signal: controller.signal,
|
||||
clear() {
|
||||
clearTimeout(timeout);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function joinUrl(baseUrl, path, params = {}) {
|
||||
let resolvedPath = String(path);
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
resolvedPath = resolvedPath.replaceAll(`{${key}}`, encodeURIComponent(String(value ?? '')));
|
||||
}
|
||||
return `${String(baseUrl).replace(/\/$/, '')}/${resolvedPath.replace(/^\//, '')}`;
|
||||
}
|
||||
|
||||
export function createLettaHttpClient({
|
||||
apiKey,
|
||||
baseUrl = 'https://api.letta.com',
|
||||
agentId = null,
|
||||
projectId = null,
|
||||
resolvePath = '/v1/agents/{agentId}/memory',
|
||||
writePath = '/v1/agents/{agentId}/messages',
|
||||
compactPath = '/v1/agents/{agentId}/memory/compact',
|
||||
modelProviderKeyId = null,
|
||||
model = null,
|
||||
modelApiType = null,
|
||||
fetchImpl = globalThis.fetch,
|
||||
timeoutMs = 3000,
|
||||
} = {}) {
|
||||
const resolvedApiKey = String(apiKey ?? '').trim();
|
||||
if (!resolvedApiKey) throw new Error('createLettaHttpClient requires MEMORY_LETTA_API_KEY');
|
||||
if (typeof fetchImpl !== 'function') throw new Error('createLettaHttpClient requires fetch');
|
||||
const resolvedAgentId = normalizeLettaId(agentId, 'MEMORY_LETTA_AGENT_ID');
|
||||
const resolvedProjectId = normalizeLettaId(projectId, 'MEMORY_LETTA_PROJECT_ID');
|
||||
|
||||
async function requestJson(method, path, body = null) {
|
||||
const timeout = timeoutSignal(Number(timeoutMs));
|
||||
try {
|
||||
const response = await fetchImpl(joinUrl(baseUrl, path, { agentId: resolvedAgentId }), {
|
||||
method,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
authorization: `Bearer ${resolvedApiKey}`,
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
...(timeout?.signal ? { signal: timeout.signal } : {}),
|
||||
});
|
||||
if (!response?.ok) {
|
||||
throw new Error(`Letta request failed: ${response?.status ?? 'unknown'}`);
|
||||
}
|
||||
return response.json?.() ?? {};
|
||||
} finally {
|
||||
timeout?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async resolve(input = {}) {
|
||||
return requestJson('POST', resolvePath, {
|
||||
user_id: input.userId,
|
||||
query: input.query,
|
||||
limit: input.limit,
|
||||
project_id: resolvedProjectId,
|
||||
model_provider_key_id: String(modelProviderKeyId ?? '').trim() || null,
|
||||
model: String(model ?? '').trim() || null,
|
||||
model_api_type: String(modelApiType ?? '').trim() || null,
|
||||
});
|
||||
},
|
||||
async write(input = {}) {
|
||||
const data = await requestJson('POST', writePath, {
|
||||
user_id: input.userId,
|
||||
session_id: input.sessionId,
|
||||
messages: Array.isArray(input.messages) ? input.messages : [],
|
||||
project_id: resolvedProjectId,
|
||||
model_provider_key_id: String(modelProviderKeyId ?? '').trim() || null,
|
||||
model: String(model ?? '').trim() || null,
|
||||
model_api_type: String(modelApiType ?? '').trim() || null,
|
||||
});
|
||||
return {
|
||||
saved: Number(data?.saved ?? data?.count ?? 1),
|
||||
analyzed: Number(data?.analyzed ?? 0),
|
||||
memories: Number(data?.memories ?? data?.memory_count ?? 0),
|
||||
};
|
||||
},
|
||||
async compact(input = {}) {
|
||||
const data = await requestJson('POST', compactPath, {
|
||||
user_id: input.userId,
|
||||
project_id: resolvedProjectId,
|
||||
model_provider_key_id: String(modelProviderKeyId ?? '').trim() || null,
|
||||
model: String(model ?? '').trim() || null,
|
||||
model_api_type: String(modelApiType ?? '').trim() || null,
|
||||
});
|
||||
return {
|
||||
analyzed: Number(data?.analyzed ?? 1),
|
||||
memories: Number(data?.memories ?? data?.memory_count ?? 0),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createLettaMemoryBackend({
|
||||
enabled = false,
|
||||
apiKey = null,
|
||||
projectId = null,
|
||||
agentId = null,
|
||||
client = null,
|
||||
unavailableReason = null,
|
||||
} = {}) {
|
||||
const hasApiKey = Boolean(String(apiKey ?? '').trim());
|
||||
const resolvedProjectId = normalizeLettaId(projectId, 'MEMORY_LETTA_PROJECT_ID');
|
||||
const resolvedAgentId = normalizeLettaId(agentId, 'MEMORY_LETTA_AGENT_ID');
|
||||
const configured = Boolean(enabled && hasApiKey);
|
||||
const hasClient = Boolean(client?.resolve && client?.write && client?.compact);
|
||||
const wired = Boolean(configured && hasClient);
|
||||
const reason = unavailableReason
|
||||
?? (enabled
|
||||
? (!hasApiKey ? 'api_key_not_configured' : 'client_not_configured')
|
||||
: 'not_configured');
|
||||
|
||||
return {
|
||||
name: 'letta',
|
||||
category: 'lifecycle',
|
||||
role: 'long-short-term-memory-os',
|
||||
flag: 'MEMORY_LETTA_ENABLED',
|
||||
unavailableReason: reason,
|
||||
projectId: resolvedProjectId,
|
||||
agentId: resolvedAgentId,
|
||||
|
||||
isAvailable() {
|
||||
return Boolean(wired);
|
||||
},
|
||||
|
||||
getUnavailableReason() {
|
||||
return this.isAvailable() ? null : reason;
|
||||
},
|
||||
|
||||
async resolve(input = {}) {
|
||||
if (!this.isAvailable()) {
|
||||
return {
|
||||
memories: [],
|
||||
semanticMemories: [],
|
||||
behaviorSummary: null,
|
||||
activeGoals: [],
|
||||
};
|
||||
}
|
||||
return normalizeResolvePayload(await client.resolve({
|
||||
...input,
|
||||
projectId: resolvedProjectId,
|
||||
agentId: resolvedAgentId,
|
||||
}));
|
||||
},
|
||||
|
||||
async write(input = {}) {
|
||||
if (!this.isAvailable()) return { saved: 0, analyzed: 0, memories: 0 };
|
||||
const result = await client.write({
|
||||
...input,
|
||||
projectId: resolvedProjectId,
|
||||
agentId: resolvedAgentId,
|
||||
});
|
||||
return {
|
||||
saved: Number(result?.saved ?? 0),
|
||||
analyzed: Number(result?.analyzed ?? 0),
|
||||
memories: Number(result?.memories ?? 0),
|
||||
};
|
||||
},
|
||||
|
||||
async compact(input = {}) {
|
||||
if (!this.isAvailable()) return { analyzed: 0, memories: 0 };
|
||||
const result = await client.compact({
|
||||
...input,
|
||||
projectId: resolvedProjectId,
|
||||
agentId: resolvedAgentId,
|
||||
});
|
||||
return {
|
||||
analyzed: Number(result?.analyzed ?? 0),
|
||||
memories: Number(result?.memories ?? 0),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user