204 lines
7.0 KiB
JavaScript
204 lines
7.0 KiB
JavaScript
// ---------------------------------------------------------------------------
|
|
// MemoryGraphAdapter -- stable interface boundary for memory graph operations.
|
|
//
|
|
// Default implementation delegates to the local JSONL-based memoryGraph.js.
|
|
// SaaS providers can supply a remote adapter by setting MEMORY_GRAPH_PROVIDER=remote
|
|
// and configuring MEMORY_GRAPH_REMOTE_URL / MEMORY_GRAPH_REMOTE_KEY.
|
|
//
|
|
// The adapter is designed so that the open-source evolver always works offline
|
|
// with the local implementation. Remote is optional and degrades gracefully.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const localGraph = require('./memoryGraph');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Adapter interface contract (all methods must be implemented by providers):
|
|
//
|
|
// getAdvice({ signals, genes, driftEnabled }) => { preferredGeneId, bannedGeneIds, currentSignalKey, explanation }
|
|
// recordSignalSnapshot({ signals, observations }) => event
|
|
// recordHypothesis({ signals, mutation, personality_state, selectedGene, selector, driftEnabled, selectedBy, capsulesUsed, observations }) => { hypothesisId, signalKey }
|
|
// recordAttempt({ signals, mutation, personality_state, selectedGene, selector, driftEnabled, selectedBy, hypothesisId, capsulesUsed, observations }) => { actionId, signalKey }
|
|
// recordOutcome({ signals, observations }) => event | null
|
|
// recordExternalCandidate({ asset, source, signals }) => event | null
|
|
// memoryGraphPath() => string
|
|
// computeSignalKey(signals) => string
|
|
// tryReadMemoryGraphEvents(limit) => event[]
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Local adapter (default) -- wraps memoryGraph.js without any behavior change
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const localAdapter = {
|
|
name: 'local',
|
|
|
|
getAdvice(opts) {
|
|
return localGraph.getMemoryAdvice(opts);
|
|
},
|
|
|
|
recordSignalSnapshot(opts) {
|
|
return localGraph.recordSignalSnapshot(opts);
|
|
},
|
|
|
|
recordHypothesis(opts) {
|
|
return localGraph.recordHypothesis(opts);
|
|
},
|
|
|
|
recordAttempt(opts) {
|
|
return localGraph.recordAttempt(opts);
|
|
},
|
|
|
|
recordOutcome(opts) {
|
|
return localGraph.recordOutcomeFromState(opts);
|
|
},
|
|
|
|
recordExternalCandidate(opts) {
|
|
return localGraph.recordExternalCandidate(opts);
|
|
},
|
|
|
|
memoryGraphPath() {
|
|
return localGraph.memoryGraphPath();
|
|
},
|
|
|
|
computeSignalKey(signals) {
|
|
return localGraph.computeSignalKey(signals);
|
|
},
|
|
|
|
tryReadMemoryGraphEvents(limit) {
|
|
return localGraph.tryReadMemoryGraphEvents(limit);
|
|
},
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Remote adapter (SaaS) -- calls external KG service with local fallback
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function buildRemoteAdapter() {
|
|
const remoteUrl = process.env.MEMORY_GRAPH_REMOTE_URL || '';
|
|
const remoteKey = process.env.MEMORY_GRAPH_REMOTE_KEY || '';
|
|
const timeoutMs = Number(process.env.MEMORY_GRAPH_REMOTE_TIMEOUT_MS) || 5000;
|
|
|
|
async function remoteCall(endpoint, body) {
|
|
if (!remoteUrl) throw new Error('MEMORY_GRAPH_REMOTE_URL not configured');
|
|
const url = `${remoteUrl.replace(/\/+$/, '')}${endpoint}`;
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(remoteKey ? { Authorization: `Bearer ${remoteKey}` } : {}),
|
|
},
|
|
body: JSON.stringify(body),
|
|
signal: controller.signal,
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`remote_kg_error: ${res.status}`);
|
|
}
|
|
return await res.json();
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
// Wrap remote call with local fallback -- ensures offline resilience.
|
|
function withFallback(localFn, remoteFn) {
|
|
return async function (...args) {
|
|
try {
|
|
return await remoteFn(...args);
|
|
} catch (e) {
|
|
// Fallback to local on any remote failure (network, timeout, config).
|
|
return localFn(...args);
|
|
}
|
|
};
|
|
}
|
|
|
|
return {
|
|
name: 'remote',
|
|
|
|
// getAdvice is the primary candidate for remote enhancement (richer graph reasoning).
|
|
getAdvice: withFallback(
|
|
(opts) => localGraph.getMemoryAdvice(opts),
|
|
async (opts) => {
|
|
const result = await remoteCall('/kg/advice', {
|
|
signals: opts.signals,
|
|
genes: (opts.genes || []).map((g) => ({ id: g.id, category: g.category, type: g.type })),
|
|
driftEnabled: opts.driftEnabled,
|
|
});
|
|
// Normalize remote response to match local contract.
|
|
return {
|
|
currentSignalKey: result.currentSignalKey || localGraph.computeSignalKey(opts.signals),
|
|
preferredGeneId: result.preferredGeneId || null,
|
|
bannedGeneIds: new Set(result.bannedGeneIds || []),
|
|
explanation: Array.isArray(result.explanation) ? result.explanation : [],
|
|
};
|
|
}
|
|
),
|
|
|
|
// Write operations: always write locally first, then async-sync to remote.
|
|
// This preserves the append-only local graph as source of truth.
|
|
recordSignalSnapshot(opts) {
|
|
const ev = localGraph.recordSignalSnapshot(opts);
|
|
remoteCall('/kg/ingest', { kind: 'signal', event: ev }).catch(() => {});
|
|
return ev;
|
|
},
|
|
|
|
recordHypothesis(opts) {
|
|
const result = localGraph.recordHypothesis(opts);
|
|
remoteCall('/kg/ingest', { kind: 'hypothesis', event: result }).catch(() => {});
|
|
return result;
|
|
},
|
|
|
|
recordAttempt(opts) {
|
|
const result = localGraph.recordAttempt(opts);
|
|
remoteCall('/kg/ingest', { kind: 'attempt', event: result }).catch(() => {});
|
|
return result;
|
|
},
|
|
|
|
recordOutcome(opts) {
|
|
const ev = localGraph.recordOutcomeFromState(opts);
|
|
if (ev) {
|
|
remoteCall('/kg/ingest', { kind: 'outcome', event: ev }).catch(() => {});
|
|
}
|
|
return ev;
|
|
},
|
|
|
|
recordExternalCandidate(opts) {
|
|
const ev = localGraph.recordExternalCandidate(opts);
|
|
if (ev) {
|
|
remoteCall('/kg/ingest', { kind: 'external_candidate', event: ev }).catch(() => {});
|
|
}
|
|
return ev;
|
|
},
|
|
|
|
memoryGraphPath() {
|
|
return localGraph.memoryGraphPath();
|
|
},
|
|
|
|
computeSignalKey(signals) {
|
|
return localGraph.computeSignalKey(signals);
|
|
},
|
|
|
|
tryReadMemoryGraphEvents(limit) {
|
|
return localGraph.tryReadMemoryGraphEvents(limit);
|
|
},
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Provider resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function resolveAdapter() {
|
|
const provider = (process.env.MEMORY_GRAPH_PROVIDER || 'local').toLowerCase().trim();
|
|
if (provider === 'remote') {
|
|
return buildRemoteAdapter();
|
|
}
|
|
return localAdapter;
|
|
}
|
|
|
|
const adapter = resolveAdapter();
|
|
|
|
module.exports = adapter;
|