@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
136 lines • 6.36 kB
JavaScript
/**
* Framework-independent GenUI owned by the Adonis agent: schemas, catalogs, presentations,
* renderers and UI tools. The same local definitions can be imported by browser code.
*/
import { assertGenuiChannels } from './channels.js';
import { defineCatalog, genuiTools, } from './public.js';
import { defineSandbox, sandboxPolicyOf, } from './sandbox.js';
export { AgentGenui, } from './factory.js';
export * from './public.js';
/**
* Picks the catalog for ONE request — a tenant's own components, a plan's versions. Extend it and
* pass the class as `resolver`: the provider builds it through the container, so it can `@inject()`
* whatever it reads (a model, a service).
*/
export class GenuiCatalogResolver {
}
/** What every configured sandbox (top level and channels) needs from the browser, together. */
function mergeSandboxClients(clients) {
const merged = { theme: false };
for (const client of clients) {
const one = client();
merged.theme ||= one.theme;
if (one.name !== undefined && merged.kit === undefined && one.kit !== undefined)
merged.name = one.name;
if (one.kit !== undefined)
merged.kit ??= one.kit;
if (one.css !== undefined)
merged.css ??= one.css;
if (one.tailwind !== undefined)
merged.tailwind ??= one.tailwind;
}
return merged;
}
function isResolverClass(value) {
return typeof value === 'function' && value.prototype instanceof GenuiCatalogResolver;
}
async function resolveCatalogFn(resolver, ctx) {
if (resolver === undefined)
return undefined;
if (resolver instanceof GenuiCatalogResolver)
return (scope) => resolver.resolve(scope);
if (isResolverClass(resolver)) {
const instance = await ctx.make(resolver);
return (scope) => instance.resolve(scope);
}
return resolver;
}
/**
* Generative UI for `config/agent.ts`:
*
* ```ts
* import { genui } from '@adonis-agora/agent/genui'
* import { catalog } from '#genui/catalog' // defineCatalog([...]) — shared with the browser
*
* export default defineConfig({ model, genui: genui({ catalog }) })
* ```
*
* Registers one `ui__render` tool taking a component tree composed from the catalog (or, with
* `mode: 'per-component'`, a `ui__show_<component>` tool per model-facing component; and/or the
* generic `ui__show` with `showTool: true`). Each call validates
* against the catalog and pushes through `ctx.emitUi`, so it streams as a `ui` frame and is persisted
* on the message. Tools without `roles` get the config's `defaultRoles`, like every other tool.
*/
export function genui(options = {}) {
return async (ctx) => {
const { catalog: given, resolver, sandbox: asked, channels: askedChannels, sandboxKit: _urls, ...rest } = options;
assertGenuiChannels(askedChannels);
const base = given ?? defineCatalog([]);
// A sandbox's kit (styles, components, wrapper) and theme names are found through the app's Vite setup.
const clients = [];
const discoveries = new Map();
const resolveSandbox = async (value) => {
if (value === undefined || value === false)
return value;
// Node-only, and only with a sandbox: kept out of a browser bundle of this module.
const [{ adonisSandboxKitDiscovery }, { resolveSandboxServer }] = await Promise.all([
import('./kit/adonis.js'),
import('./kit/discover.js'),
]);
const kitName = value === true ? undefined : value.kitName;
let discovery = discoveries.get(kitName ?? '');
if (discovery === undefined) {
discovery = await adonisSandboxKitDiscovery(ctx.app, {
...(kitName !== undefined ? { name: kitName } : {}),
...(options.sandboxKit ?? {}),
});
discoveries.set(kitName ?? '', discovery);
}
const resolved = resolveSandboxServer(value, discovery);
clients.push(resolved.client);
return resolved.define;
};
const sandbox = await resolveSandbox(asked);
// A kit sandbox defined in the (shared) catalog itself: found the same way, and put
// back under its own name, so the browser's copy of the catalog validates what the model writes.
const own = base.components.find((each) => sandboxPolicyOf(each) !== undefined);
const ownOptions = asked === undefined && own?.sandboxOptions !== undefined && own.sandboxView?.kit === true
? await resolveSandbox({ ...own.sandboxOptions, name: own.name })
: undefined;
let channels;
if (askedChannels !== undefined) {
channels = {};
for (const [name, entry] of Object.entries(askedChannels)) {
if (entry === undefined)
continue;
const own = await resolveSandbox(entry.sandbox);
channels[name] = { ...entry, ...(own !== undefined ? { sandbox: own } : {}) };
}
}
const catalog = sandbox === undefined || sandbox === false
? typeof ownOptions === 'object'
? base.extend([defineSandbox(ownOptions)])
: base
: base.extend([defineSandbox(sandbox === true ? {} : sandbox)]);
const resolveCatalog = await resolveCatalogFn(resolver, ctx);
const tools = genuiTools(catalog, {
...rest,
...(channels !== undefined ? { channels } : {}),
...(sandbox !== undefined ? { sandbox } : {}),
...(resolveCatalog !== undefined ? { resolveCatalog } : {}),
});
return {
catalog,
...(resolveCatalog === undefined ? {} : { resolveCatalog }),
tools,
...(channels !== undefined ? { channels } : {}),
...(clients.length > 0 ? { sandboxClient: () => mergeSandboxClients(clients) } : {}),
base: {
...(options.mode !== undefined ? { mode: options.mode } : {}),
...(options.streaming !== undefined ? { streaming: options.streaming } : {}),
...(sandbox !== undefined ? { sandbox } : {}),
},
};
};
}
//# sourceMappingURL=index.js.map