@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
189 lines • 8.7 kB
JavaScript
import { standaloneComponents, toolNameFor } from './catalog.js';
import { toJsonSchema } from './schema.js';
import { GENUI_TREE_COMPONENT } from './tree.js';
/**
* Plain text (Slack mrkdwn compatible) for a pushed component: its definition's `fallbackText`, or
* the props as a JSON block when the catalog has no text for it. A `fallbackText` that returns an
* empty (or blank) string means "nothing to say" — a bare layout like `Stack` — and renders nothing.
* A tree frame ({@link GENUI_TREE_COMPONENT}) renders node by node, so a layout's children still
* render. Never throws — a fallback that fails prints the JSON instead.
*/
export function componentToText(catalog, component, props) {
if (component === GENUI_TREE_COMPONENT) {
const root = props.root;
return root === undefined ? '' : treeToText(catalog, root);
}
return nodeText(catalog, component, props) ?? '';
}
/**
* One component's own text (children aside): its `fallbackText` (`''` when blank — nothing to say),
* the props as JSON when it has none or it throws, or `undefined` for a layout node without text.
*/
function nodeText(catalog, component, props) {
const definition = catalog.get(component);
if (definition?.fallbackText !== undefined) {
try {
const text = definition.fallbackText(props);
if (typeof text === 'string')
return text.trim().length > 0 ? text : '';
}
catch {
/* fall through to the generic rendering */
}
return jsonBlock(props);
}
return definition?.children === true ? undefined : jsonBlock(props);
}
/** Plain text for a composed tree: each node's text, depth-first, blank-line free. */
export function treeToText(catalog, root) {
const out = [];
const visit = (node, depth) => {
if (depth > 32)
return;
const text = nodeText(catalog, node.type, node.props);
if (text !== undefined)
out.push(text);
for (const child of node.children ?? [])
visit(child, depth + 1);
};
visit(root, 0);
return out.filter((line) => line.trim().length > 0).join('\n');
}
function jsonBlock(props) {
const title = typeof props.title === 'string' && props.title.length > 0 ? `*${props.title}*\n` : '';
return `${title}\`\`\`\n${JSON.stringify(props, null, 2)}\n\`\`\``;
}
/**
* The catalog described for a model: one entry per model-facing component with its description and
* a compact TypeScript-like signature of its props. What a tree-mode (or show) tool puts in its
* description, and what a host can add to a system prompt.
*/
export function catalogToModelText(catalog, options = {}) {
const mode = options.mode ?? 'tree';
const lines = [];
if (mode === 'tree') {
lines.push('Compose the UI as ONE tree of elements: { "type": <component>, "props": { … }, "children"?: [ … ] }.', 'Only components marked "takes children" accept children; `children` is a literal JSON array, never a string.', 'Every element starts with "type", then "props" — a single component is { "type": …, "props": { … } } too, never its props alone. A component\'s own field order (if its description gives one) is the order of the keys INSIDE its "props".', ...treeExamples(catalog), 'Components:');
}
else if (mode === 'show') {
lines.push('Pass { "component": <name>, "props": { … } } with one of these components:');
}
else {
lines.push('Components you can show (one tool each):');
}
// Outside a tree nothing nests, so a layout (`children: true`) is not offered on its own.
const components = mode === 'tree' ? catalog.modelComponents() : standaloneComponents(catalog);
for (const component of components) {
const schema = toJsonSchema(component.props);
const head = mode === 'per-component'
? `- ${component.name} (tool \`${toolNameFor(component.name, options.namePrefix)}\`)`
: `- ${component.name}`;
lines.push(`${head}: ${component.description}`);
if (schema !== undefined) {
lines.push(` props: ${summarizeSchema(schema, schema, 0)}`);
}
if (mode === 'tree' && component.children === true) {
lines.push(' takes children');
}
}
return lines.join('\n');
}
/**
* One or two literal calls built from the catalog's own components — the envelope shown rather
* than described: a one-component tree, and a layout holding it when the catalog has one.
*/
function treeExamples(catalog) {
const components = catalog.modelComponents();
const leaves = components.filter((component) => component.children !== true && requiredKeys(component).length > 0);
// A component drawn while it is written (the sandbox) is the one a model most often sends as
// bare props — long, written field by field — so it is the one worth showing wrapped.
const leaf = leaves.find((component) => component.partialProps !== undefined) ?? leaves[0];
if (leaf === undefined)
return [];
const props = `{ ${requiredKeys(leaf)
.map((key) => `"${key}": …`)
.join(', ')} }`;
const element = `{ "type": "${leaf.name}", "props": ${props} }`;
const layout = components.find((component) => component.children === true);
return [
`Example, one component: ${element}`,
...(layout !== undefined
? [
`Example, nested: { "type": "${layout.name}", "props": { … }, "children": [ ${element} ] }`,
]
: []),
];
}
function requiredKeys(component) {
const schema = toJsonSchema(component.props);
return Array.isArray(schema?.required)
? schema.required.filter((key) => typeof key === 'string')
: [];
}
/** A JSON Schema as a one-line TypeScript-like type, e.g. `{ title?: string, rows: object[] }`. */
export function summarizeSchema(schema, root, depth) {
if (typeof schema !== 'object' || schema === null)
return 'unknown';
const s = schema;
if (depth > 5)
return '…';
if (typeof s.$ref === 'string') {
return s.$ref === '#' ? 'Self' : (s.$ref.split('/').pop() ?? 'unknown');
}
if (Array.isArray(s.enum)) {
return s.enum.map((option) => JSON.stringify(option)).join(' | ');
}
if ('const' in s)
return JSON.stringify(s.const);
const variants = (s.anyOf ?? s.oneOf);
if (Array.isArray(variants)) {
return variants.map((variant) => summarizeSchema(variant, root, depth + 1)).join(' | ');
}
const types = Array.isArray(s.type) ? s.type : s.type !== undefined ? [s.type] : [];
if (types.length > 1) {
return types.map((type) => summarizeSchema({ ...s, type }, root, depth + 1)).join(' | ');
}
const type = types[0];
if (type === 'array') {
const item = s.items === undefined ? 'unknown' : summarizeSchema(s.items, root, depth + 1);
return /[|\s]/.test(item) && !item.startsWith('{') ? `(${item})[]` : `${item}[]`;
}
if (type === 'object' || (type === undefined && s.properties !== undefined)) {
const properties = (s.properties ?? {});
const keys = Object.keys(properties);
if (keys.length === 0)
return 'object';
const required = new Set(Array.isArray(s.required) ? s.required : []);
const fields = keys.map((key) => {
const field = properties[key];
const note = typeof field?.description === 'string' && depth === 0 ? ` /* ${field.description} */` : '';
return `${key}${required.has(key) ? '' : '?'}: ${summarizeSchema(field, root, depth + 1)}${note}`;
});
return `{ ${fields.join(', ')} }`;
}
if (type === 'integer')
return 'number';
return type ?? 'unknown';
}
/** Replace `{{path}}` placeholders with prop values (objects as JSON). */
export function fillTemplate(template, props) {
return template.replace(/\{\{\s*([\w.[\]]+)\s*\}\}/g, (_match, path) => {
const value = getPath(props, path);
if (value === null || value === undefined)
return '';
return typeof value === 'object' ? JSON.stringify(value) : String(value);
});
}
/** Read `a.b[0].c` from a value; `undefined` when any step is missing. */
export function getPath(source, path) {
let current = source;
for (const part of path
.replace(/\[(\d+)\]/g, '.$1')
.split('.')
.filter(Boolean)) {
if (current === null || current === undefined)
return undefined;
current = current[part];
}
return current;
}
//# sourceMappingURL=text.js.map