function normalizePolicyId(value) { const raw = String(value ?? '').trim(); if (!raw) return null; if (!/^[a-zA-Z0-9_-]+$/.test(raw)) { throw new Error(`Invalid MEMORY_LANGGRAPH_POLICY_ID: ${raw}`); } return raw; } 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) { return `${String(baseUrl).replace(/\/$/, '')}/${String(path).replace(/^\//, '')}`; } export function createLangGraphHttpClient({ baseUrl, apiKey = null, resolvePath = '/memory/resolve', modelProviderKeyId = null, model = null, modelApiType = null, fetchImpl = globalThis.fetch, timeoutMs = 3000, } = {}) { const resolvedBaseUrl = String(baseUrl ?? '').trim(); if (!resolvedBaseUrl) throw new Error('createLangGraphHttpClient requires MEMORY_LANGGRAPH_URL'); if (typeof fetchImpl !== 'function') throw new Error('createLangGraphHttpClient requires fetch'); return { async resolve(input = {}) { const timeout = timeoutSignal(Number(timeoutMs)); try { const response = await fetchImpl(joinUrl(resolvedBaseUrl, resolvePath), { method: 'POST', headers: { 'content-type': 'application/json', ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}), }, body: JSON.stringify({ user_id: input.userId, session_id: input.sessionId, query: input.query, policy_id: input.policyId, limit: input.limit, model_provider_key_id: String(modelProviderKeyId ?? '').trim() || null, model: String(model ?? '').trim() || null, model_api_type: String(modelApiType ?? '').trim() || null, }), ...(timeout?.signal ? { signal: timeout.signal } : {}), }); if (!response?.ok) { throw new Error(`LangGraph request failed: ${response?.status ?? 'unknown'}`); } return response.json?.() ?? {}; } finally { timeout?.clear(); } }, }; } export function createLangGraphMemoryBackend({ enabled = false, policyId = null, client = null, unavailableReason = null, } = {}) { const resolvedPolicyId = normalizePolicyId(policyId); const hasClient = Boolean(client?.resolve); const wired = Boolean(enabled && hasClient); const reason = unavailableReason ?? (enabled ? 'client_not_configured' : 'not_configured'); return { name: 'langgraph', category: 'policy', role: 'routing-reasoning-policy', flag: 'MEMORY_LANGGRAPH_ENABLED', unavailableReason: reason, policyId: resolvedPolicyId, isAvailable() { return Boolean(wired); }, getUnavailableReason() { return this.isAvailable() ? null : reason; }, async resolve(input = {}) { if (!this.isAvailable()) { return { memories: [], semanticMemories: [], behaviorSummary: null, activeGoals: [] }; } const payload = await client.resolve({ ...input, policyId: resolvedPolicyId }); return { memories: Array.isArray(payload?.memories) ? payload.memories : [], semanticMemories: Array.isArray(payload?.semanticMemories) ? payload.semanticMemories : [], behaviorSummary: payload?.behaviorSummary ?? null, activeGoals: Array.isArray(payload?.activeGoals) ? payload.activeGoals : [], }; }, }; }