@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
657 lines • 28.4 kB
JavaScript
import { readUiAction, readUiActionText, } from '../genui/actions.js';
import { toJsonSchema } from '../genui/schema.js';
import { componentToText } from '../genui/text.js';
import { GENUI_TREE_COMPONENT } from '../genui/tree.js';
import { resolveToolConfirmation } from '../tool-presentation.js';
/** The A2UI protocol version every message is stamped with. */
export const A2UI_VERSION = 'v0.9';
/** The catalog id of A2UI v0.9's basic catalog (what `@a2ui/react`'s `basicCatalog.id` is). */
export const A2UI_BASIC_CATALOG_ID = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json';
/**
* The id the AG-UI A2UI binding knows the basic catalog by: `@ag-ui/a2ui-middleware`,
* `@ag-ui/a2ui-toolkit` and CopilotKit's A2UI renderer (1.77, on `@a2ui/web_core` 0.10) register it
* under this earlier v0.9 id, not {@link A2UI_BASIC_CATALOG_ID}. The AG-UI transport's default.
*/
export const A2UI_LEGACY_BASIC_CATALOG_ID = 'https://a2ui.org/specification/v0_9/basic_catalog.json';
/** Every id the basic catalog goes by: the current one first. */
export const A2UI_BASIC_CATALOG_IDS = Object.freeze([
A2UI_BASIC_CATALOG_ID,
A2UI_LEGACY_BASIC_CATALOG_ID,
]);
/** Whether a catalog id names A2UI v0.9's basic catalog (under either of its ids). */
export function isA2uiBasicCatalogId(id) {
return typeof id === 'string' && A2UI_BASIC_CATALOG_IDS.includes(id);
}
/** The components of A2UI v0.9's basic catalog. */
export const A2UI_BASIC_COMPONENTS = [
'Text',
'Image',
'Icon',
'Video',
'AudioPlayer',
'Row',
'Column',
'List',
'Card',
'Tabs',
'Modal',
'Divider',
'Button',
'CheckBox',
'TextField',
'DateTimeInput',
'ChoicePicker',
'Slider',
];
/** The AG-UI activity type A2UI surfaces travel under (AG-UI's A2UI middleware, CopilotKit). */
export const A2UI_ACTIVITY_TYPE = 'a2ui-surface';
/** The key of the A2UI messages in an `a2ui-surface` activity's content. */
export const A2UI_OPERATIONS_KEY = 'a2ui_operations';
/** Action names the transports answer themselves: deciding a parked approval. */
export const A2UI_APPROVE_ACTION = 'agora.approve';
export const A2UI_REJECT_ACTION = 'agora.reject';
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
const str = (value, fallback = '') => typeof value === 'string' ? value : typeof value === 'number' ? String(value) : fallback;
function text(id, value, variant) {
return { id, component: 'Text', text: value, ...(variant !== undefined ? { variant } : {}) };
}
/** A column of `[heading?, ...children]` — what most builtins come down to. */
function titled(ctx, title, children, extra = []) {
const heading = typeof title === 'string' && title.length > 0;
const titleId = ctx.derive('title');
return [
{
id: ctx.id,
component: 'Column',
children: heading ? [titleId, ...children] : [...children],
},
...(heading ? [text(titleId, title, 'h4')] : []),
...extra,
];
}
function cards(ctx, items, lines) {
const list = Array.isArray(items) ? items.filter(isRecord) : [];
const out = [];
const ids = [];
list.forEach((item, index) => {
const card = ctx.derive(`item${index}`);
const column = ctx.derive(`item${index}col`);
const parts = lines(item).filter(([value]) => value.length > 0);
ids.push(card);
out.push({ id: card, component: 'Card', child: column });
out.push({
id: column,
component: 'Column',
children: parts.map((_, line) => ctx.derive(`item${index}l${line}`)),
});
parts.forEach(([value, variant], line) => {
out.push(text(ctx.derive(`item${index}l${line}`), value, variant));
});
});
return [{ id: ctx.id, component: 'Row', children: ids }, ...out];
}
function table(ctx, title, columns, rows) {
const out = [];
const header = ctx.derive('header');
const rowIds = rows.map((_, index) => ctx.derive(`row${index}`));
out.push({
id: header,
component: 'Row',
children: columns.map((_, index) => ctx.derive(`h${index}`)),
});
columns.forEach((column, index) => {
out.push({ ...text(ctx.derive(`h${index}`), column.label, 'caption'), weight: 1 });
});
rows.forEach((row, r) => {
out.push({
id: rowIds[r],
component: 'Row',
children: columns.map((_, c) => ctx.derive(`r${r}c${c}`)),
});
columns.forEach((column, c) => {
out.push({ ...text(ctx.derive(`r${r}c${c}`), str(row[column.key])), weight: 1 });
});
});
return titled(ctx, title, [header, ...rowIds], out);
}
function columnsOf(value) {
return (Array.isArray(value) ? value : [])
.filter(isRecord)
.filter((column) => typeof column.key === 'string')
.map((column) => ({
key: column.key,
label: str(column.label, column.key),
}));
}
function rowsOf(value) {
return (Array.isArray(value) ? value : []).filter(isRecord);
}
/** The library builtins, mapped onto A2UI's basic catalog. */
export const A2UI_BUILTIN_MAPPERS = Object.freeze({
Stack: (props, ctx) => [
{
id: ctx.id,
component: props.direction === 'row' ? 'Row' : 'Column',
children: [...ctx.children],
},
],
Card: (props, ctx) => {
const inner = ctx.derive('body');
const subtitle = ctx.derive('subtitle');
const hasSubtitle = typeof props.subtitle === 'string' && props.subtitle.length > 0;
return [
{ id: ctx.id, component: 'Card', child: inner },
...titled({ ...ctx, id: inner }, props.title, hasSubtitle ? [subtitle, ...ctx.children] : ctx.children, hasSubtitle ? [text(subtitle, props.subtitle, 'caption')] : []),
];
},
Heading: (props, ctx) => [text(ctx.id, str(props.text), 'h3')],
Text: (props, ctx) => [text(ctx.id, str(props.text), props.muted === true ? 'caption' : 'body')],
Badge: (props, ctx) => [text(ctx.id, str(props.text), 'caption')],
Callout: (props, ctx) => {
const body = ctx.derive('body');
const message = ctx.derive('text');
return [
{ id: ctx.id, component: 'Card', child: body },
...titled({ ...ctx, id: body }, props.title, [message], [text(message, str(props.text))]),
];
},
Link: (props, ctx) => {
const label = ctx.derive('label');
return [
{
id: ctx.id,
component: 'Button',
variant: 'borderless',
child: label,
action: { functionCall: { call: 'openUrl', args: { url: str(props.url) } } },
},
text(label, str(props.text, str(props.url))),
];
},
Image: (props, ctx) => [
{
id: ctx.id,
component: 'Image',
url: str(props.url),
...(typeof props.alt === 'string' ? { description: props.alt } : {}),
},
],
CodeBlock: (props, ctx) => [text(ctx.id, `\`\`\`\n${str(props.code)}\n\`\`\``)],
KpiCards: (props, ctx) => cards(ctx, props.items, (item) => [
[str(item.label), 'caption'],
[str(item.value), 'h3'],
[str(item.delta), 'caption'],
]),
SourceCards: (props, ctx) => cards(ctx, props.items, (item) => [
[str(item.title), 'h5'],
[str(item.snippet), 'body'],
[str(item.source, str(item.url)), 'caption'],
]),
DataTable: (props, ctx) => table(ctx, props.title, columnsOf(props.columns), rowsOf(props.rows)),
// The basic catalog has no chart: its points, as a table — what a text client gets too.
Chart: (props, ctx) => {
const xKey = str(props.xKey);
const series = (Array.isArray(props.series) ? props.series : [])
.filter(isRecord)
.filter((each) => typeof each.key === 'string')
.map((each) => ({ key: each.key, label: str(each.label, each.key) }));
return table(ctx, props.title, [{ key: xKey, label: xKey }, ...series], rowsOf(props.data));
},
});
/** Ids A2UI renderers accept everywhere: the node ids (`root.0.1`) and derived ones (`root.0~title`). */
function safeId(id) {
return id.replace(/[^A-Za-z0-9_.~:-]/g, '_');
}
/**
* A `ui` frame as A2UI components, flat, the root's id `root`. A composed tree (`genui:tree`) maps
* node by node — node ids are their positions (`root`, `root.0`, `root.0.2`), stable from the first
* preview to the final frame, so a streamed surface only ever grows. A node still held back
* (`held`) is left out: its parent references an id that arrives later, which A2UI draws as a
* placeholder.
*/
export function toA2uiComponents(frame, options = {}) {
const props = isRecord(frame.props) ? frame.props : {};
const root = frame.component === GENUI_TREE_COMPONENT
? isRecord(props.root) && typeof props.root.type === 'string'
? props.root
: undefined
: { type: frame.component, props };
if (root === undefined)
return [];
const out = [];
const visit = (node, id, depth) => {
if (depth > 32 || node.held === true)
return;
const children = (Array.isArray(node.children) ? node.children : []).filter((child) => isRecord(child) && typeof child.type === 'string');
const childIds = children.map((_, index) => `${id}.${index}`);
const nodeProps = isRecord(node.props) ? node.props : {};
const ctx = {
id,
children: childIds,
derive: (suffix) => `${id}~${suffix}`,
incomplete: node.incomplete === true,
text: () => options.catalog !== undefined ? componentToText(options.catalog, node.type, nodeProps) : '',
};
out.push(...mapNode(node.type, nodeProps, ctx, options));
children.forEach((child, index) => {
visit(child, childIds[index], depth + 1);
});
};
visit(root, 'root', 1);
return out.map((component) => ({ ...component, id: safeId(component.id) })).map(fixRefs);
}
function fixRefs(component) {
const next = { ...component };
if (Array.isArray(next.children))
next.children = next.children.map((id) => safeId(String(id)));
for (const key of ['child', 'trigger', 'content']) {
if (typeof next[key] === 'string')
next[key] = safeId(next[key]);
}
return next;
}
function mapNode(type, props, ctx, options) {
const own = options.components?.[type];
if (typeof own === 'function')
return withRoot(own(props, ctx), ctx, type);
if (own === 'custom' || (own === undefined && options.unmapped === 'custom')) {
return [
{
...props,
id: ctx.id,
component: type,
...(ctx.children.length > 0 ? { children: [...ctx.children] } : {}),
},
];
}
const builtin = A2UI_BUILTIN_MAPPERS[type];
if (builtin !== undefined)
return withRoot(builtin(props, ctx), ctx, type);
// Nothing maps it: its words, then whatever it holds.
const words = ctx.text();
if (ctx.children.length === 0)
return [text(ctx.id, words)];
const label = ctx.derive('text');
return [
{
id: ctx.id,
component: 'Column',
children: words.length > 0 ? [label, ...ctx.children] : [...ctx.children],
},
...(words.length > 0 ? [text(label, words)] : []),
];
}
function withRoot(components, ctx, type) {
if (!components.some((component) => component.id === ctx.id)) {
throw new Error(`a2ui: the mapping of ${type} must return a component with id "${ctx.id}"`);
}
return components;
}
/**
* {@link toA2uiComponents} that never throws: a mapping that fails (an app mapper that throws or
* forgets the root id) draws the component's text instead of breaking the stream it rides on.
*/
function safeComponents(frame, options) {
try {
return toA2uiComponents(frame, options);
}
catch {
const props = isRecord(frame.props) ? frame.props : {};
const words = options.catalog !== undefined ? componentToText(options.catalog, frame.component, props) : '';
return words.length > 0 ? [text('root', words)] : [];
}
}
/** The messages that draw (or redraw) a surface: `createSurface` the first time, then its components. */
export function a2uiSurfaceMessages(surfaceId, components, options) {
const out = [];
if (options.create) {
out.push({
version: A2UI_VERSION,
createSurface: {
surfaceId,
catalogId: options.catalogId ?? options.basicCatalogId ?? A2UI_BASIC_CATALOG_ID,
...(options.theme !== undefined ? { theme: options.theme } : {}),
...(options.sendDataModel === true ? { sendDataModel: true } : {}),
},
});
}
if (components.length > 0) {
out.push({ version: A2UI_VERSION, updateComponents: { surfaceId, components } });
}
return out;
}
/** A surface id A2UI clients accept, from a `ui` frame id (`<toolCallId>:ui:0`). */
export function a2uiSurfaceId(frameId) {
return frameId.replace(/[^A-Za-z0-9_.~:-]/g, '_');
}
/**
* A2UI's AG-UI binding (the AG-UI A2UI middleware, CopilotKit's renderer): one `ACTIVITY_SNAPSHOT`
* of type `a2ui-surface` per `ui` frame, carrying the whole surface (`createSurface` +
* `updateComponents`) with `replace: true` — so each preview of a streaming tree repaints the same
* activity in place, keyed by the frame id.
*/
export function a2uiActivityEvent(frame, options = {}) {
const props = isRecord(frame.props) ? frame.props : {};
const surfaceId = a2uiSurfaceId(frame.id);
// A withdrawn preview: the surface it painted goes.
if (frame.partial === true && Object.keys(props).length === 0) {
return {
type: 'ACTIVITY_SNAPSHOT',
messageId: `a2ui-surface-${surfaceId}`,
activityType: A2UI_ACTIVITY_TYPE,
content: { [A2UI_OPERATIONS_KEY]: [{ version: A2UI_VERSION, deleteSurface: { surfaceId } }] },
replace: true,
};
}
const components = safeComponents(frame, options);
if (components.length === 0)
return null;
return {
type: 'ACTIVITY_SNAPSHOT',
messageId: `a2ui-surface-${surfaceId}`,
activityType: A2UI_ACTIVITY_TYPE,
content: {
[A2UI_OPERATIONS_KEY]: a2uiSurfaceMessages(surfaceId, components, {
...options,
create: true,
}),
},
replace: true,
};
}
/**
* An A2UI catalog definition (v0.9 inline catalog shape) for an agent catalog's components sent
* as custom components: `{ catalogId, components: { Name: JSON Schema } }` — what a client
* registers, or hands an A2UI-aware runtime (`a2uiClientCapabilities.inlineCatalogs`).
*/
export function a2uiCatalog(catalog, options) {
const components = {};
for (const definition of catalog.modelComponents()) {
if (options.only !== undefined && !options.only.includes(definition.name))
continue;
const schema = toJsonSchema(definition.props) ?? { type: 'object' };
const properties = isRecord(schema.properties) ? schema.properties : {};
components[definition.name] = {
type: 'object',
description: definition.description,
properties: {
component: { const: definition.name },
...properties,
...(definition.children === true
? {
children: {
$ref: 'https://a2ui.org/specification/v0_9/common_types.json#/$defs/ChildList',
},
}
: {}),
},
required: ['component', ...(Array.isArray(schema.required) ? schema.required : [])],
};
}
return { catalogId: options.catalogId, components };
}
/**
* A client-to-server A2UI message, or an AG-UI `forwardedProps.a2uiAction`, as a {@link UiAction}:
* v0.9 `{ version, action: { name, surfaceId, sourceComponentId, timestamp, context } }`, v0.8
* `{ userAction: { … } }`, or the bare action object. A string when it is not one.
*/
export function readA2uiAction(raw, options = {}) {
if (!isRecord(raw))
return 'an A2UI action must be an object';
const inner = isRecord(raw.action) ? raw.action : isRecord(raw.userAction) ? raw.userAction : raw;
if (typeof inner.name !== 'string')
return 'an A2UI action carries a name';
return readUiAction({
source: 'a2ui',
name: inner.name,
context: isRecord(inner.context) ? inner.context : {},
...(typeof inner.surfaceId === 'string' ? { surfaceId: inner.surfaceId } : {}),
...(typeof inner.sourceComponentId === 'string'
? { componentId: inner.sourceComponentId }
: {}),
...(typeof inner.timestamp === 'string' ? { timestamp: inner.timestamp } : {}),
...(typeof inner.text === 'string' ? { text: inner.text } : {}),
}, { ...options, source: 'a2ui' });
}
/** The surface an approval interrupt is shown on: its message, and Approve / Reject buttons. */
export function a2uiApprovalComponents(interrupt) {
const button = (id, label, name, variant) => [
{
id,
component: 'Button',
variant,
child: `${id}~label`,
action: { event: { name, context: { interruptId: interrupt.id } } },
},
text(`${id}~label`, label),
];
// The tool's own prompt, filled on the server: its title is the message, its detail under it.
const confirmation = interrupt.metadata?.['agora.confirmation'];
const toolName = interrupt.metadata?.['agora.toolName'];
const fallback = resolveToolConfirmation(typeof toolName === 'string' ? toolName : '', interrupt.metadata?.['agora.input']);
const message = typeof confirmation?.title === 'string' && confirmation.title.length > 0
? confirmation.title
: interrupt.message !== undefined && interrupt.message.length > 0
? interrupt.message
: fallback.title;
// The tool's own verb on the button ("Refund"), else the generic one.
const verb = typeof confirmation?.verb === 'string' && confirmation.verb.length > 0
? confirmation.verb
: fallback.verb;
const detail = typeof confirmation?.detail === 'string' && confirmation.detail.length > 0
? confirmation.detail
: undefined;
return [
{ id: 'root', component: 'Card', child: 'body' },
{
id: 'body',
component: 'Column',
children: detail !== undefined ? ['message', 'detail', 'buttons'] : ['message', 'buttons'],
},
text('message', message, 'body'),
...(detail !== undefined ? [text('detail', detail, 'caption')] : []),
{ id: 'buttons', component: 'Row', children: ['approve', 'reject'] },
...button('approve', verb, A2UI_APPROVE_ACTION, 'primary'),
...button('reject', 'Reject', A2UI_REJECT_ACTION, 'default'),
];
}
/**
* One AG-UI run as an A2UI message stream — the projection the A2UI transport serves. Stateful
* (which surfaces exist), pure otherwise: feed it the run's AG-UI events in order.
*
* - `CUSTOM agora.ui` → a surface per `ui` frame id, created once and updated in place (previews
* included); a withdrawn preview deletes it;
* - text messages → a text surface each (see {@link A2uiStreamOptions.text});
* - an approval interrupt → a surface with Approve / Reject buttons ({@link A2UI_APPROVE_ACTION});
* any other interrupt → a text surface saying to answer it in the app;
* - `RUN_ERROR` → a text surface with the message.
*/
export class A2uiProjector {
options;
surfaces = new Set();
texts = new Map();
constructor(options = {}) {
this.options = options;
}
project(event) {
switch (event.type) {
case 'CUSTOM':
return event.name === 'agora.ui' ? this.ui(event.value) : [];
case 'TEXT_MESSAGE_START':
if (this.options.text === 'omit')
return [];
this.texts.set(event.messageId, '');
return this.surface(`text-${event.messageId}`, [
{ id: 'root', component: 'Text', text: { path: '/text' } },
]);
case 'TEXT_MESSAGE_CONTENT': {
if (this.options.text === 'omit')
return [];
const next = `${this.texts.get(event.messageId) ?? ''}${event.delta}`;
this.texts.set(event.messageId, next);
return [
{
version: A2UI_VERSION,
updateDataModel: { surfaceId: `text-${event.messageId}`, path: '/text', value: next },
},
];
}
case 'RUN_FINISHED': {
const outcome = event.outcome;
if (!isRecord(outcome) || outcome.type !== 'interrupt')
return [];
const interrupts = Array.isArray(outcome.interrupts) ? outcome.interrupts : [];
return interrupts.filter(isRecord).flatMap((interrupt) => {
const id = a2uiSurfaceId(`interrupt-${String(interrupt.id)}`);
return interrupt.reason === 'tool_approval'
? this.surface(id, a2uiApprovalComponents(interrupt))
: this.surface(id, [
text('root', `${str(interrupt.message, 'The assistant is waiting for an answer.')} (answer it in the app)`),
]);
});
}
case 'RUN_ERROR':
return this.surface(`error-${this.surfaces.size}`, [text('root', event.message)]);
default:
return [];
}
}
surface(surfaceId, components) {
const create = !this.surfaces.has(surfaceId);
this.surfaces.add(surfaceId);
return a2uiSurfaceMessages(surfaceId, components, {
...this.options,
// Text, approvals and errors are basic-catalog components whatever the app's catalog is.
catalogId: this.options.basicCatalogId ?? A2UI_BASIC_CATALOG_ID,
create,
});
}
ui(value) {
if (!isRecord(value) || typeof value.id !== 'string' || typeof value.component !== 'string') {
return [];
}
const surfaceId = a2uiSurfaceId(value.id);
const props = isRecord(value.props) ? value.props : {};
if (value.partial === true && Object.keys(props).length === 0) {
if (!this.surfaces.delete(surfaceId))
return [];
return [{ version: A2UI_VERSION, deleteSurface: { surfaceId } }];
}
const components = safeComponents({ component: value.component, props: props }, this.options);
if (components.length === 0)
return [];
const create = !this.surfaces.has(surfaceId);
this.surfaces.add(surfaceId);
return a2uiSurfaceMessages(surfaceId, components, { ...this.options, create });
}
}
function catalogIdsOf(value) {
if (!isRecord(value) || !Array.isArray(value.supportedCatalogIds))
return undefined;
return value.supportedCatalogIds.filter((id) => typeof id === 'string');
}
/**
* The catalog ids a client says it draws, from its `a2uiClientCapabilities`: v0.9's
* `{ 'v0.9': { supportedCatalogIds } }` (what `MessageProcessor.getRendererCapabilities()` builds),
* a `v0.9.1` entry, or a flat `{ supportedCatalogIds }`. `undefined` when it names none.
*/
export function readA2uiClientCapabilities(raw) {
if (!isRecord(raw))
return undefined;
const ids = [
...(catalogIdsOf(raw['v0.9']) ?? []),
...(catalogIdsOf(raw['v0.9.1']) ?? []),
...(catalogIdsOf(raw) ?? []),
];
return ids.length > 0 ? [...new Set(ids)] : undefined;
}
/** The AG-UI context entry CopilotKit's A2UI provider sends ("A2UI catalog capabilities: …"). */
const AG_UI_CATALOG_CONTEXT = /^A2UI catalog capabilities/i;
/**
* The catalog ids an AG-UI client advertises: `forwardedProps.a2uiClientCapabilities`, or the
* `context` entry CopilotKit's A2UI provider adds (its value lists the ids, one `- <id>` per line).
*/
export function readAgUiA2uiCatalogIds(input) {
const forwarded = isRecord(input.forwardedProps)
? readA2uiClientCapabilities(input.forwardedProps.a2uiClientCapabilities)
: undefined;
if (forwarded !== undefined)
return forwarded;
const ids = [];
for (const entry of input.context ?? []) {
if (!isRecord(entry) || typeof entry.description !== 'string')
continue;
if (!AG_UI_CATALOG_CONTEXT.test(entry.description) || typeof entry.value !== 'string')
continue;
for (const line of entry.value.split('\n')) {
const id = /^\s*-\s+(\S+:\/\/\S+)/.exec(line)?.[1];
if (id !== undefined)
ids.push(id);
}
}
return ids.length > 0 ? [...new Set(ids)] : undefined;
}
/**
* Options for one client: the basic catalog under the id the client advertises (else the
* configured `basicCatalogId`, a basic `catalogId`, or the transport's `fallback`), and `catalogId`
* resolved — an app catalog stays as configured; the basic catalog takes the negotiated id.
*/
export function negotiateA2uiCatalog(options, supported, fallback = A2UI_BASIC_CATALOG_ID) {
const advertised = (supported ?? []).filter(isA2uiBasicCatalogId);
const preferred = [options.basicCatalogId, options.catalogId].find((id) => id !== undefined && advertised.includes(id));
const basic = preferred ??
advertised[0] ??
options.basicCatalogId ??
(isA2uiBasicCatalogId(options.catalogId) ? options.catalogId : undefined) ??
fallback;
const catalogId = options.catalogId === undefined || isA2uiBasicCatalogId(options.catalogId)
? basic
: options.catalogId;
return { ...options, basicCatalogId: basic, catalogId };
}
/**
* A stored thread as A2UI, for a client that reopens it: each user message as its line, each
* assistant step as the surfaces the live stream drew — its text (one `Text`), then every `ui`
* frame it kept (the last props per id), mapped exactly as the stream maps them, under the same
* surface ids. An approval the thread is still waiting on is not redrawn: ask again by sending.
*/
export function a2uiThreadReplay(messages, options = {}) {
const out = [];
const basic = options.basicCatalogId ?? A2UI_BASIC_CATALOG_ID;
for (const message of messages) {
if (message.role === 'user') {
const action = readUiActionText(message.content);
out.push({
role: 'user',
id: message.id,
text: action?.text ?? message.content,
...(action !== null ? { action } : {}),
});
continue;
}
if (message.role !== 'assistant')
continue;
const surfaces = [];
if (options.text !== 'omit' && message.content.trim().length > 0) {
surfaces.push(...a2uiSurfaceMessages(`text-${message.id}`, [text('root', message.content)], {
...options,
catalogId: basic,
create: true,
}));
}
for (const frame of message.ui ?? []) {
if (frame.partial === true)
continue;
const components = safeComponents(frame, options);
if (components.length === 0)
continue;
surfaces.push(...a2uiSurfaceMessages(a2uiSurfaceId(frame.id), components, { ...options, create: true }));
}
if (surfaces.length > 0)
out.push({ role: 'assistant', id: message.id, messages: surfaces });
}
return out;
}
//# sourceMappingURL=core.js.map