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), }; }, }; }