@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
190 lines • 8.04 kB
JavaScript
import { useEffect, useMemo, useState } from 'react';
import { GENUI_TREE_COMPONENT, } from './types.js';
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* Normalize whatever carries a pushed component: a transcript `ui` block, a `data-ui` message part
* (`{ type: 'data-ui', id, data }`), a stored `{ id, component, props, version? }` entry. `null` for
* anything else.
*/
export function toGenerativeUIItem(part) {
if (!isRecord(part))
return null;
const source = part.type === 'data-ui' && isRecord(part.data) ? part.data : part;
if (typeof source.component !== 'string')
return null;
const id = typeof source.id === 'string' ? source.id : typeof part.id === 'string' ? part.id : null;
if (id === null)
return null;
return {
id,
component: source.component,
...(isRecord(source.componentVersions) &&
Object.values(source.componentVersions).every((version) => typeof version === 'number' && Number.isSafeInteger(version) && version > 0)
? { componentVersions: { ...source.componentVersions } }
: {}),
...(typeof source.fallbackText === 'string' ? { fallbackText: source.fallbackText } : {}),
props: isRecord(source.props) ? source.props : {},
version: typeof source.version === 'number' ? source.version : null,
toolCallId: typeof source.toolCallId === 'string' ? source.toolCallId : null,
...(source.partial === true ? { partial: true } : {}),
};
}
const resolverCaches = new WeakMap();
function resolveCached(resolve, name, version) {
let cache = resolverCaches.get(resolve);
if (cache === undefined) {
cache = new Map();
resolverCaches.set(resolve, cache);
}
const key = `${name}@${version ?? ''}`;
const hit = cache.get(key);
if (hit !== undefined)
return hit;
let entry;
try {
const result = resolve(name, version);
if (result instanceof Promise) {
entry = { resolution: { status: 'loading' }, settled: null };
entry.settled = result.then((Component) => {
entry.resolution = Component ? { status: 'ready', Component } : { status: 'unknown' };
}, (error) => {
entry.resolution = { status: 'error', error };
});
}
else {
entry = {
resolution: result ? { status: 'ready', Component: result } : { status: 'unknown' },
settled: null,
};
}
}
catch (error) {
entry = { resolution: { status: 'error', error }, settled: null };
}
cache.set(key, entry);
return entry;
}
/**
* Which renderer draws `name`: the registry first, then `resolveComponent` (cached; async results
* re-render the caller when they land). `fallbackTree` answers the tree component when the
* registry does not override it.
*/
export function useResolvedComponent(name, version, registry, resolve, fallbackTree) {
const [, setTick] = useState(0);
const own = registry[name];
const entry = own !== undefined || resolve === undefined ? null : resolveCached(resolve, name, version);
const pending = entry?.resolution.status === 'loading' ? entry.settled : null;
useEffect(() => {
if (pending === null)
return;
let live = true;
void pending.then(() => {
if (live)
setTick((tick) => tick + 1);
});
return () => {
live = false;
};
}, [pending]);
if (own !== undefined)
return { status: 'ready', Component: own };
if (entry !== null && entry.resolution.status !== 'unknown')
return entry.resolution;
if (name === GENUI_TREE_COMPONENT && fallbackTree !== undefined) {
return { status: 'ready', Component: fallbackTree };
}
return { status: 'unknown' };
}
/**
* Props checked against the catalog: synchronously when the catalog can (`validateSync`), else
* after the async check settles. A component the catalog does not know passes through unchecked.
*/
export function useValidatedProps(catalog, name, props) {
const checked = catalog?.has(name) === true;
const immediate = useMemo(() => {
if (!checked || catalog === undefined)
return { status: 'ok', props };
const result = catalog.validateSync?.(name, props);
if (result === undefined)
return null;
return result.ok
? { status: 'ok', props: result.value }
: { status: 'invalid', issues: result.issues };
}, [checked, catalog, name, props]);
const [late, setLate] = useState(null);
useEffect(() => {
if (immediate !== null || catalog === undefined)
return;
let live = true;
catalog.validate(name, props).then((result) => {
if (!live)
return;
setLate({
props,
result: result.ok
? { status: 'ok', props: result.value }
: { status: 'invalid', issues: result.issues },
});
}, (error) => {
if (!live)
return;
setLate({
props,
result: {
status: 'invalid',
issues: [{ path: [], message: error instanceof Error ? error.message : String(error) }],
},
});
});
return () => {
live = false;
};
}, [immediate, catalog, name, props]);
if (immediate !== null)
return immediate;
return late !== null && late.props === props ? late.result : { status: 'pending' };
}
/** {@link import('./generative-ui.js').useGenerativeUI}, with the tree renderer handed in (avoids an import cycle). */
export function useGenerativeUIState(part, options, fallbackTree) {
const item = useMemo(() => toGenerativeUIItem(part), [part]);
const name = item?.component ?? '';
const definition = options.catalog?.get?.(name);
const incompatible = item?.fallbackText !== undefined &&
item.version !== null &&
definition !== undefined &&
item.version !== (definition.version ?? 1);
const resolution = useResolvedComponent(name, item?.version ?? null, incompatible ? {} : options.registry, item === null ? undefined : options.resolveComponent, fallbackTree);
const empty = useMemo(() => ({}), []);
// A tree is validated node by node as it renders, not as one component.
const validation = useValidatedProps(item === null || name === GENUI_TREE_COMPONENT || incompatible ? undefined : options.catalog, name, item?.props ?? empty);
if (item === null)
return null;
// A withdrawn preview (partial, nothing to draw): the call will not replace it.
if (item.partial === true && Object.keys(item.props).length === 0)
return null;
if (name === GENUI_TREE_COMPONENT &&
item.fallbackText !== undefined &&
(!isRecord(item.props.root) || typeof item.props.root.type !== 'string'))
return {
status: 'problem',
reason: 'invalid',
item,
issues: [{ path: ['root'], message: 'Invalid UI tree root' }],
};
if (resolution.status === 'loading')
return { status: 'loading', item };
if (resolution.status === 'unknown')
return { status: 'problem', reason: 'unknown', item };
if (resolution.status === 'error') {
return { status: 'problem', reason: 'error', item, error: resolution.error };
}
if (validation.status === 'pending')
return { status: 'loading', item };
if (validation.status === 'invalid') {
return { status: 'problem', reason: 'invalid', item, issues: validation.issues };
}
return { status: 'ready', item, Component: resolution.Component, props: validation.props };
}
//# sourceMappingURL=use-generative-ui.js.map