UNPKG

@adonis-agora/agent

Version:

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

151 lines • 7.19 kB
import { Chart, DataTable } from './builtins.js'; import { defineCatalog, defineComponent, } from './catalog.js'; import { formatIssues, validateProps } from './schema.js'; import { componentToText } from './text.js'; /** Reject values JSON would silently discard/coerce, and detach from caller-owned objects. */ export function snapshotComponentPresentation(value) { const seen = new Set(); const check = (entry) => { if (entry === null || typeof entry === 'string' || typeof entry === 'boolean') return; if (typeof entry === 'number' && Number.isFinite(entry)) return; if (typeof entry !== 'object' || entry === null) throw new TypeError('genui: presentation must contain only JSON values'); if (seen.has(entry)) throw new TypeError('genui: presentation must contain acyclic JSON values'); if (!Array.isArray(entry) && Object.getPrototypeOf(entry) !== Object.prototype && Object.getPrototypeOf(entry) !== null) throw new TypeError('genui: presentation must contain plain JSON objects'); seen.add(entry); for (const item of Object.values(entry)) check(item); seen.delete(entry); }; check(value); return JSON.parse(JSON.stringify(value)); } export function createComponent(definition, options = {}) { defineComponent(definition); const catalog = defineCatalog([definition], options); const factory = async (props) => { const result = await validateProps(definition.props, props, catalog.validator); if (!result.ok) throw new Error(`genui: invalid ${definition.name} props: ${formatIssues(result.issues)}`); const normalized = snapshotComponentPresentation(result.value); if (typeof normalized !== 'object' || normalized === null || Array.isArray(normalized)) throw new TypeError('genui: component props must be a JSON object'); const portable = await catalog.validate(definition.name, normalized); if (!portable.ok || canonicalJson(snapshotComponentPresentation(portable.value)) !== canonicalJson(normalized)) { throw new Error(`genui: ${definition.name} requires outputProps accepting stable normalized JSON props`); } return { component: definition.name, props: normalized, version: definition.version ?? 1, fallbackText: componentToText(catalog, definition.name, normalized), }; }; return Object.assign(factory, { definition }); } /** A registry is owned by one app/tenant; registering never mutates a shared singleton. */ export function createComponentRegistry(options = {}) { let catalog = defineCatalog([], options); const channels = new Map(); const registry = { get catalog() { return catalog; }, get manifest() { return catalog.components.map((definition) => { const props = catalog.jsonSchemaFor(definition.name); return { name: definition.name, title: definition.title, description: definition.description, version: definition.version ?? 1, ...(props === undefined ? {} : { props: snapshotComponentPresentation(props) }), }; }); }, register(definition, renderers) { if (catalog.has(definition.name)) throw new Error(`genui: component "${definition.name}" is already registered`); catalog = catalog.extend([definition]); channels.set(definition.name, { ...renderers }); return this; }, async prepare(presentation) { const definition = catalog.get(presentation.component); if (definition === undefined) throw new Error(`genui: unknown component "${presentation.component}"`); if (presentation.version !== (definition.version ?? 1)) throw new Error(`genui: component "${presentation.component}" version mismatch`); const validated = await catalog.validate(presentation.component, presentation.props); if (!validated.ok) throw new Error(`genui: invalid ${presentation.component} props: ${formatIssues(validated.issues)}`); const props = snapshotComponentPresentation(validated.value); if (typeof props !== 'object' || props === null || Array.isArray(props)) throw new TypeError('genui: component props must be a JSON object'); return { component: definition.name, props, version: definition.version ?? 1, fallbackText: componentToText(catalog, definition.name, props), }; }, async render(presentation, channel, context) { const prepared = await this.prepare(presentation); const mapping = channels.get(prepared.component); const renderer = mapping === undefined ? undefined : Object.hasOwn(mapping, channel) ? mapping[channel] : Object.hasOwn(mapping, 'text') ? mapping.text : undefined; return renderer === undefined ? prepared.fallbackText : renderer(prepared.props, context); }, }; return registry; } export const table = createComponent(DataTable); export const chart = createComponent(Chart); /** Prepare all JSON emissions before any sink receives a partial batch. Factories validate schemas. */ export function validatePresentationBatch(presentations) { const batch = Array.isArray(presentations) ? presentations : [presentations]; return batch.map((item) => { const presentation = snapshotComponentPresentation(item); if (typeof presentation !== 'object' || presentation === null || typeof presentation.component !== 'string' || !/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(presentation.component) || !Number.isSafeInteger(presentation.version) || presentation.version <= 0 || typeof presentation.fallbackText !== 'string' || typeof presentation.props !== 'object' || presentation.props === null || Array.isArray(presentation.props)) { throw new TypeError('genui: invalid component presentation'); } return presentation; }); } /** Object order is immaterial; array order remains part of the portable contract. */ function canonicalJson(value) { if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`; if (typeof value === 'object' && value !== null) { const record = value; return `{${Object.keys(record) .sort() .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`) .join(',')}}`; } return JSON.stringify(value); } //# sourceMappingURL=registry.js.map