UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

304 lines • 11.8 kB
export function isStandardSchema(schema) { return (typeof schema === 'object' && schema !== null && '~standard' in schema && typeof schema['~standard']?.validate === 'function'); } /** * The JSON Schema a props schema presents to a model: the schema itself for a JSON Schema, the * Standard JSON Schema converter's output for a Standard Schema that has one (Zod 4, Valibot, * ArkType). `undefined` when neither is available (e.g. Zod 3) — the model then gets the * component's description only. */ export function toJsonSchema(schema) { if (!isStandardSchema(schema)) { return schema; } const standard = schema['~standard']; const converter = standard.jsonSchema; if (converter === undefined || typeof converter.input !== 'function') { return undefined; } try { return converter.input({ target: 'draft-07' }); } catch { return undefined; } } /** Validate against either kind of props schema. A Standard Schema's own (possibly transformed) output wins. */ export async function validateProps(schema, value, validator) { if (isStandardSchema(schema)) { const result = await schema['~standard'].validate(value); if (result.issues !== undefined) { return { ok: false, issues: toGenuiIssues(result.issues) }; } return { ok: true, value: result.value }; } const issues = validator.validate(schema, value); return issues.length > 0 ? { ok: false, issues } : { ok: true, value }; } /** * {@link validateProps} without waiting: the verdict when it is available synchronously (always * for JSON Schema, and for every Standard Schema whose `validate` does not return a promise — Zod, * Valibot and ArkType all answer synchronously for synchronous schemas), `undefined` otherwise. What * a renderer uses to decide on its first paint instead of flashing a placeholder. */ export function validatePropsSync(schema, value, validator) { if (!isStandardSchema(schema)) { const issues = validator.validate(schema, value); return issues.length > 0 ? { ok: false, issues } : { ok: true, value }; } const result = schema['~standard'].validate(value); if (result instanceof Promise) { result.catch(() => undefined); return undefined; } return result.issues !== undefined ? { ok: false, issues: toGenuiIssues(result.issues) } : { ok: true, value: result.value }; } function toGenuiIssues(issues) { return issues.map((issue) => ({ path: (issue.path ?? []).map((segment) => typeof segment === 'object' && segment !== null && 'key' in segment ? segment.key : segment), message: issue.message, })); } /** `a.b[0].c: message; …` — what a model is handed back when its props were refused. */ export function formatIssues(issues) { return issues .map((issue) => `${issue.path.length > 0 ? formatPath(issue.path) : '(root)'}: ${issue.message}`) .join('; '); } function formatPath(path) { return path .map((segment, index) => typeof segment === 'number' ? `[${segment}]` : index === 0 ? segment : `.${segment}`) .join(''); } /** * Full JSON Schema validation through the app's own Ajv instance. Compiled validators are cached * per schema object. */ export function ajvValidator(ajv) { const compiled = new WeakMap(); return { validate(schema, value) { let fn = compiled.get(schema); if (fn === undefined) { fn = ajv.compile(schema); compiled.set(schema, fn); } if (fn(value) === true) { return []; } return (fn.errors ?? []).map((error) => ({ path: (error.instancePath ?? '') .split('/') .filter((segment) => segment.length > 0) .map((segment) => { const decoded = segment.replace(/~1/g, '/').replace(/~0/g, '~'); return /^\d+$/.test(decoded) ? Number(decoded) : decoded; }), message: error.message ?? 'is invalid', })); }, }; } /** * A dependency-free JSON Schema validator for the keywords component props actually use: `type` * (incl. `integer`, `null`, type arrays), `enum`, `const`, `properties`, `required`, * `additionalProperties`, `items`, `minItems`/`maxItems`, `minLength`/`maxLength`/`pattern`, * `minimum`/`maximum`, `anyOf`/`oneOf`/`allOf`, `not`, and local `$ref`s (`#`, `#/$defs/…`, * `#/definitions/…`). Unknown keywords (e.g. `format`) are ignored. Use {@link ajvValidator} when * you need the rest. */ export const builtinJsonSchemaValidator = { validate(schema, value) { const issues = []; check(schema, value, [], schema, issues, 0); return issues; }, }; const MAX_DEPTH = 64; function typeOf(value) { if (value === null) return 'null'; if (Array.isArray(value)) return 'array'; return typeof value; } function matchesType(type, value) { switch (type) { case 'integer': return typeof value === 'number' && Number.isInteger(value); case 'number': return typeof value === 'number' && Number.isFinite(value); case 'object': return typeof value === 'object' && value !== null && !Array.isArray(value); default: return typeOf(value) === type; } } function resolveRef(root, ref) { if (ref === '#') return root; if (!ref.startsWith('#/')) return undefined; let current = root; for (const raw of ref.slice(2).split('/')) { const key = raw.replace(/~1/g, '/').replace(/~0/g, '~'); if (typeof current !== 'object' || current === null) return undefined; current = current[key]; } return typeof current === 'object' && current !== null ? current : undefined; } function deepEqual(a, b) { if (a === b) return true; if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false; if (Array.isArray(a) !== Array.isArray(b)) return false; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; return keysA.every((key) => deepEqual(a[key], b[key])); } function check(schema, value, path, root, issues, depth) { if (schema === true || schema === undefined) return; if (schema === false) { issues.push({ path, message: 'is not allowed' }); return; } if (typeof schema !== 'object' || schema === null) return; if (depth > MAX_DEPTH) { issues.push({ path, message: 'is nested too deeply' }); return; } const s = schema; if (typeof s.$ref === 'string') { const target = resolveRef(root, s.$ref); if (target === undefined) { issues.push({ path, message: `references unknown schema ${s.$ref}` }); return; } check(target, value, path, root, issues, depth + 1); } if (s.type !== undefined) { const types = Array.isArray(s.type) ? s.type : [s.type]; if (!types.some((type) => matchesType(type, value))) { issues.push({ path, message: `must be ${types.join(' or ')}` }); return; } } if (Array.isArray(s.enum) && !s.enum.some((option) => deepEqual(option, value))) { issues.push({ path, message: `must be one of ${s.enum.map((option) => JSON.stringify(option)).join(', ')}`, }); } if ('const' in s && !deepEqual(s.const, value)) { issues.push({ path, message: `must be ${JSON.stringify(s.const)}` }); } if (typeof value === 'string') { if (typeof s.minLength === 'number' && value.length < s.minLength) { issues.push({ path, message: `must be at least ${s.minLength} characters` }); } if (typeof s.maxLength === 'number' && value.length > s.maxLength) { issues.push({ path, message: `must be at most ${s.maxLength} characters` }); } if (typeof s.pattern === 'string') { let pattern; try { pattern = new RegExp(s.pattern, 'u'); } catch { pattern = undefined; } if (pattern !== undefined && !pattern.test(value)) { issues.push({ path, message: `must match ${s.pattern}` }); } } } if (typeof value === 'number') { if (typeof s.minimum === 'number' && value < s.minimum) { issues.push({ path, message: `must be >= ${s.minimum}` }); } if (typeof s.maximum === 'number' && value > s.maximum) { issues.push({ path, message: `must be <= ${s.maximum}` }); } } if (Array.isArray(value)) { if (typeof s.minItems === 'number' && value.length < s.minItems) { issues.push({ path, message: `must have at least ${s.minItems} items` }); } if (typeof s.maxItems === 'number' && value.length > s.maxItems) { issues.push({ path, message: `must have at most ${s.maxItems} items` }); } if (s.items !== undefined && !Array.isArray(s.items)) { value.forEach((item, index) => { check(s.items, item, [...path, index], root, issues, depth + 1); }); } } if (matchesType('object', value)) { const record = value; const properties = typeof s.properties === 'object' && s.properties !== null ? s.properties : {}; if (Array.isArray(s.required)) { for (const key of s.required) { if (record[key] === undefined) { issues.push({ path: [...path, key], message: 'is required' }); } } } for (const [key, entry] of Object.entries(record)) { if (key in properties) { if (entry !== undefined) { check(properties[key], entry, [...path, key], root, issues, depth + 1); } } else if (s.additionalProperties === false) { issues.push({ path: [...path, key], message: 'is not a known property' }); } else if (typeof s.additionalProperties === 'object' && s.additionalProperties !== null) { check(s.additionalProperties, entry, [...path, key], root, issues, depth + 1); } } } if (Array.isArray(s.allOf)) { for (const sub of s.allOf) check(sub, value, path, root, issues, depth + 1); } if (Array.isArray(s.anyOf) || Array.isArray(s.oneOf)) { const options = (Array.isArray(s.anyOf) ? s.anyOf : s.oneOf); const passing = options.filter((sub) => { const nested = []; check(sub, value, path, root, nested, depth + 1); return nested.length === 0; }).length; if (Array.isArray(s.anyOf) ? passing === 0 : passing !== 1) { issues.push({ path, message: Array.isArray(s.anyOf) ? 'does not match any allowed shape' : 'must match exactly one allowed shape', }); } } if (s.not !== undefined) { const nested = []; check(s.not, value, path, root, nested, depth + 1); if (nested.length === 0) { issues.push({ path, message: 'matches a shape that is not allowed' }); } } } //# sourceMappingURL=schema.js.map