125 lines
3.6 KiB
JavaScript
125 lines
3.6 KiB
JavaScript
const ALLOWED_OPERATIONS = ['resolve', 'write', 'compact'];
|
|
const ALLOWED_PLUGIN_CATEGORIES = new Set([
|
|
'semantic',
|
|
'extraction',
|
|
'lifecycle',
|
|
'behavior',
|
|
'policy',
|
|
]);
|
|
|
|
function check(name, ok, message = null, details = {}) {
|
|
return { name, ok: Boolean(ok), ...(message ? { message } : {}), ...details };
|
|
}
|
|
|
|
function isValidName(value) {
|
|
return /^[a-z][a-z0-9-]*$/.test(String(value ?? ''));
|
|
}
|
|
|
|
function supports(backend, operation) {
|
|
return typeof backend?.[operation] === 'function';
|
|
}
|
|
|
|
function safeAvailable(backend) {
|
|
try {
|
|
return backend?.isAvailable?.() !== false;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function inspectMemoryV2Backend(backend) {
|
|
const name = String(backend?.name ?? '').trim();
|
|
const available = safeAvailable(backend);
|
|
const support = Object.fromEntries(
|
|
ALLOWED_OPERATIONS.map((operation) => [operation, supports(backend, operation)]),
|
|
);
|
|
const checks = [
|
|
check('name_present', Boolean(name), 'Backend name is required'),
|
|
check('name_format', isValidName(name), 'Backend name must be lowercase kebab-case', {
|
|
backendName: name,
|
|
}),
|
|
check(
|
|
'has_operation',
|
|
Object.values(support).some(Boolean),
|
|
'Backend must implement at least one Memory V2 operation',
|
|
{ supports: support },
|
|
),
|
|
];
|
|
|
|
if (!available) {
|
|
const reason = typeof backend?.getUnavailableReason === 'function'
|
|
? backend.getUnavailableReason()
|
|
: backend?.unavailableReason;
|
|
checks.push(check(
|
|
'unavailable_reason',
|
|
Boolean(reason),
|
|
'Unavailable backend must expose a reason',
|
|
{ reason: reason ?? null },
|
|
));
|
|
}
|
|
|
|
if (backend?.category != null) {
|
|
checks.push(check(
|
|
'category_known',
|
|
ALLOWED_PLUGIN_CATEGORIES.has(String(backend.category)),
|
|
'Plugin backend category is unknown',
|
|
{ category: String(backend.category) },
|
|
));
|
|
}
|
|
|
|
if (backend?.flag != null) {
|
|
checks.push(check(
|
|
'flag_format',
|
|
/^[A-Z0-9_]+$/.test(String(backend.flag)),
|
|
'Backend feature flag must be SCREAMING_SNAKE_CASE',
|
|
{ flag: String(backend.flag) },
|
|
));
|
|
}
|
|
|
|
return {
|
|
ok: checks.every((item) => item.ok),
|
|
name,
|
|
available,
|
|
supports: support,
|
|
category: backend?.category ?? null,
|
|
role: backend?.role ?? null,
|
|
flag: backend?.flag ?? null,
|
|
checks,
|
|
};
|
|
}
|
|
|
|
export function validateMemoryV2BackendSet(backends = []) {
|
|
const inspected = backends.map((backend) => inspectMemoryV2Backend(backend));
|
|
const names = inspected.map((item) => item.name).filter(Boolean);
|
|
const duplicateNames = names.filter((name, index) => names.indexOf(name) !== index);
|
|
const checks = [
|
|
check('backend_list_present', Array.isArray(backends), 'Backends must be an array'),
|
|
check('backend_list_non_empty', inspected.length > 0, 'At least one backend is required'),
|
|
check('backend_names_unique', duplicateNames.length === 0, 'Backend names must be unique', {
|
|
duplicateNames: [...new Set(duplicateNames)],
|
|
}),
|
|
check(
|
|
'legacy_backend_present',
|
|
names.includes('legacy-conversation-memory'),
|
|
'Legacy conversation memory backend must remain registered',
|
|
),
|
|
];
|
|
|
|
return {
|
|
ok: checks.every((item) => item.ok) && inspected.every((item) => item.ok),
|
|
checkedAt: new Date().toISOString(),
|
|
summary: {
|
|
backendCount: inspected.length,
|
|
availableBackends: inspected.filter((item) => item.available).map((item) => item.name),
|
|
operationSupport: Object.fromEntries(
|
|
ALLOWED_OPERATIONS.map((operation) => [
|
|
operation,
|
|
inspected.filter((item) => item.supports[operation]).map((item) => item.name),
|
|
]),
|
|
),
|
|
},
|
|
checks,
|
|
backends: inspected,
|
|
};
|
|
}
|