@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
JavaScript
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