@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
277 lines • 14.9 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { Component, createContext, memo, useCallback, useContext, useMemo, useRef, } from 'react';
import { AmbientRenderUiContext, sharedContext } from '../components/ambient-ui.js';
import { shareStructure } from './share-structure.js';
import { useGenerativeUIState, useResolvedComponent, useValidatedProps, } from './use-generative-ui.js';
const TreeScopeContext = createContext(null);
/** Where UI actions go — `GenuiActionProvider` (sandbox.tsx) and `GenuiProvider`'s `onAction`. */
export const GenuiActionContext = sharedContext('@adonis-agora/agent/react:genui-action');
// Shared by key, like the provider's: a renderer imports the hook from `/genui` while the tree may
// be drawn by the `/genui/json-render` bundle's copy of this module.
const GenuiNodeContext = sharedContext('@adonis-agora/agent/react:genui-node');
/**
* The tree node being rendered — read it in a renderer to draw a skeleton while the model is still
* writing the node (`incomplete`), e.g. a chart's axes before its data. `null` outside a tree.
*
* ```tsx
* function Chart(props: ChartProps) {
* const node = useGenuiNode();
* if (node?.incomplete && !props.data?.length) return <ChartSkeleton title={props.title} />;
* return <BarChart {...props} />;
* }
* ```
*/
export function useGenuiNode() {
return useContext(GenuiNodeContext);
}
/**
* Set the node a renderer sees through {@link useGenuiNode} — for glue that draws components outside
* a `genui:tree` frame (a framework's own tool-call renderer) and knows the call is still streaming:
* `<GenuiNodeScope node={{ id: 'root', type: 'Sandbox', incomplete: true, held: false }}>`.
*/
export function GenuiNodeScope({ node, children, }) {
return _jsx(GenuiNodeContext.Provider, { value: node, children: children });
}
function renderPlaceholder(look, node) {
const { placeholder } = look;
if (typeof placeholder === 'function')
return placeholder(node);
return placeholder !== undefined ? placeholder : look.loading;
}
function renderFallback(fallback, problem) {
if (typeof fallback === 'function')
return fallback(problem);
if (fallback !== undefined)
return fallback;
return problem.item.fallbackText !== undefined ? (_jsx("span", { style: { whiteSpace: 'pre-wrap' }, children: problem.item.fallbackText })) : null;
}
/** One failing renderer takes down its own item, never the message around it. */
class ItemBoundary extends Component {
state = { error: undefined, failed: false };
static getDerivedStateFromError(error) {
return { error, failed: true };
}
componentDidCatch(error, _info) {
this.props.onError?.(error, this.props.item);
}
componentDidUpdate(previous) {
// New props for the same item (a streamed update) get a fresh chance to render.
if (this.state.failed && previous.item !== this.props.item) {
this.setState({ error: undefined, failed: false });
}
}
render() {
if (this.state.failed) {
return renderFallback(this.props.fallback, {
reason: 'error',
item: this.props.item,
error: this.state.error,
});
}
return this.props.children;
}
}
function nodeItem(node, id) {
return { id, component: node.type, props: node.props ?? {}, version: null, toolCallId: null };
}
/** A failing node takes the whole tree to the item's fallback text — no fallback of its own given. */
function failsWhole(scope) {
return !scope.hasFallback && scope.fallbackText !== undefined;
}
/**
* One tree node, re-rendered only when its node is a different object (or the scope changed).
* {@link GenuiTree} keeps an unchanged node the same object from frame to frame, so as a preview
* streams in only the nodes that grew — and their ancestors — render again; a node turning from
* `incomplete`/`held` to final is a changed node and renders.
*/
const TreeNode = memo(function TreeNode({ node, id, bounded }) {
const scope = useContext(TreeScopeContext);
const item = useMemo(() => nodeItem(node, id), [node, id]);
if (scope === null)
return null;
if (!bounded || failsWhole(scope))
return _jsx(TreeNodeBody, { node: node, id: id, item: item });
return (_jsx(ItemBoundary, { item: item, fallback: scope.look.current.fallback, onError: scope.look.current.onError, children: _jsx(TreeNodeBody, { node: node, id: id, item: item }) }));
}, (previous, next) => previous.node === next.node && previous.id === next.id && previous.bounded === next.bounded);
/** A tree node resolved and validated like a top-level item, children rendered as its `children`. */
function TreeNodeBody({ node, id, item, }) {
const scope = useContext(TreeScopeContext);
// A node of a preview the model is still writing: its props are half there, so they are not
// validated (the final frame is) — the renderer is told instead, through `useGenuiNode`.
const incomplete = node.incomplete === true;
const held = node.held === true;
const state = useMemo(() => ({ id, type: node.type, incomplete, held, ...(incomplete ? { streamSafe: true } : {}) }), [id, node.type, incomplete, held]);
const storedVersion = scope?.componentVersions?.[node.type];
const definition = scope?.options.catalog?.get?.(node.type);
const incompatible = storedVersion !== undefined &&
definition !== undefined &&
storedVersion !== (definition.version ?? 1);
const resolution = useResolvedComponent(node.type, storedVersion ?? null, incompatible ? {} : (scope?.options.registry ?? {}), scope?.options.resolveComponent);
const validation = useValidatedProps(incompatible || incomplete ? undefined : scope?.options.catalog, node.type, item.props);
if (scope === null)
return null;
const look = scope.look.current;
if (held) {
return (_jsx(GenuiNodeContext.Provider, { value: state, children: renderPlaceholder(look, state) }));
}
if (resolution.status === 'loading' || validation.status === 'pending')
return look.loading;
if (resolution.status === 'unknown') {
if (failsWhole(scope))
throw new Error('Unknown tree component');
return renderFallback(look.fallback, { reason: 'unknown', item });
}
if (resolution.status === 'error') {
if (failsWhole(scope))
throw resolution.error;
return renderFallback(look.fallback, { reason: 'error', item, error: resolution.error });
}
if (validation.status === 'invalid') {
if (failsWhole(scope))
throw new Error('Invalid tree component');
return renderFallback(look.fallback, { reason: 'invalid', item, issues: validation.issues });
}
const Renderer = resolution.Component;
const children = Array.isArray(node.children) ? node.children : [];
return (_jsx(GenuiNodeContext.Provider, { value: state, children: _jsx(Renderer, { ...validation.props, children: children.length > 0
? children.map((child, index) => {
const childId = `${id}.${index}`;
return _jsx(TreeNode, { node: child, id: childId, bounded: true }, childId);
})
: undefined }) }));
}
/**
* Renders a `genui:tree` frame's `{ root }` node by node through the same registry, catalog and
* resolver as top-level components. Used automatically for tree frames unless a `treeRenderer`
* (e.g. the json-render one) or a registry entry for `genui:tree` takes over.
*
* Each frame's tree is structurally shared with the previous one: a node equal to the one before
* keeps that object, and its renderer does not run again.
*/
export function GenuiTree({ root }) {
const previous = useRef(undefined);
const valid = root !== undefined && root !== null && typeof root.type === 'string';
const shared = valid ? shareStructure(previous.current, root) : undefined;
previous.current = shared;
if (shared === undefined)
return null;
return _jsx(TreeNode, { node: shared, id: "root", bounded: false });
}
/**
* The registry, catalog and fallbacks tree nodes render with. `<GenerativeUI>` provides it; wrap
* your own chrome in it when you draw `useGenerativeUI`'s `Component` yourself and it may be a tree.
*/
export function GenerativeUIScope({ registry, catalog, resolveComponent, fallback, fallbackText, componentVersions, loading = null, placeholder, onError, children, }) {
// Read afresh from every frame of the item that carries them: kept the same object while equal.
const versionsRef = useRef(undefined);
const versions = shareStructure(versionsRef.current, componentVersions);
versionsRef.current = versions;
const look = useRef({ fallback, loading, placeholder, onError });
look.current = { fallback, loading, placeholder, onError };
const hasFallback = fallback !== undefined;
const scope = useMemo(() => ({
options: {
registry,
...(catalog !== undefined ? { catalog } : {}),
...(resolveComponent !== undefined ? { resolveComponent } : {}),
},
hasFallback,
...(versions !== undefined ? { componentVersions: versions } : {}),
...(fallbackText !== undefined ? { fallbackText } : {}),
look,
}), [registry, catalog, resolveComponent, hasFallback, fallbackText, versions]);
return _jsx(TreeScopeContext.Provider, { value: scope, children: children });
}
// Shared by key: `/genui` and `/genui/json-render` are separate bundles, and a provider from one
// must reach a `<GenerativeUI>` from the other.
const GenuiContext = sharedContext('@adonis-agora/agent/react:genui-provider');
/** The enclosing `<GenuiProvider>`'s settings, or `null` outside one. */
export function useGenuiProvider() {
return useContext(GenuiContext);
}
/**
* Set generative UI up once, at the app root: the registry of your renderers, the catalog to
* validate against, a resolver for components the registry lacks, and what to draw when one does
* not render. Every `<GenerativeUI>` below reads it (its own props still win), and `MessageItem` /
* `MessageList` draw pushed components with it — no `renderUi` per message.
*
* ```tsx
* <GenuiProvider registry={registry} catalog={catalog} fallback={({ item }) => <Unknown name={item.component} />}>
* <App />
* </GenuiProvider>
* ```
*/
export function GenuiProvider({ registry, catalog, resolveComponent, treeRenderer, fallback, loading, placeholder, onError, onAction, children, }) {
const value = useMemo(() => ({
...(registry !== undefined ? { registry } : {}),
...(catalog !== undefined ? { catalog } : {}),
...(resolveComponent !== undefined ? { resolveComponent } : {}),
...(treeRenderer !== undefined ? { treeRenderer } : {}),
...(fallback !== undefined ? { fallback } : {}),
...(loading !== undefined ? { loading } : {}),
...(placeholder !== undefined ? { placeholder } : {}),
...(onError !== undefined ? { onError } : {}),
}), [registry, catalog, resolveComponent, treeRenderer, fallback, loading, placeholder, onError]);
const renderUi = useCallback((block) => _jsx(GenerativeUI, { part: block }), []);
const inner = (_jsx(AmbientRenderUiContext.Provider, { value: renderUi, children: children }));
return (_jsx(GenuiContext.Provider, { value: value, children: onAction !== undefined ? (_jsx(GenuiActionContext.Provider, { value: onAction, children: inner })) : (inner) }));
}
const NO_REGISTRY = {};
/** Provider settings overlaid with explicit ones (explicit wins), memoized on their parts. */
function useMergedOptions(own) {
const provided = useContext(GenuiContext);
const registry = own.registry ?? provided?.registry ?? NO_REGISTRY;
const catalog = own.catalog ?? provided?.catalog;
const resolveComponent = own.resolveComponent ?? provided?.resolveComponent;
const treeRenderer = own.treeRenderer ?? provided?.treeRenderer;
return useMemo(() => ({
registry,
...(catalog !== undefined ? { catalog } : {}),
...(resolveComponent !== undefined ? { resolveComponent } : {}),
...(treeRenderer !== undefined ? { treeRenderer } : {}),
}), [registry, catalog, resolveComponent, treeRenderer]);
}
/**
* The headless half of {@link GenerativeUI}: what to draw for one pushed component, or `null` when
* `part` is not one. Options default to the enclosing `<GenuiProvider>`'s.
*/
export function useGenerativeUI(part, options = {}) {
const merged = useMergedOptions(options);
return useGenerativeUIState(part, merged, merged.treeRenderer ?? GenuiTree);
}
/**
* Draws one server-pushed component with the app's own renderer. Headless: it adds no element and
* no style of its own — only what the registry's component renders (and whatever `fallback` /
* `loading` you pass). Everything but `part` defaults to the enclosing `<GenuiProvider>`.
*
* ```tsx
* <GenerativeUI part={block} />
* ```
*/
export function GenerativeUI({ part, registry, catalog, resolveComponent, treeRenderer, fallback: ownFallback, loading: ownLoading, placeholder: ownPlaceholder, onError: ownOnError, }) {
const provided = useContext(GenuiContext);
const fallback = ownFallback !== undefined ? ownFallback : provided?.fallback;
const loading = ownLoading ?? provided?.loading ?? null;
const placeholder = ownPlaceholder !== undefined ? ownPlaceholder : provided?.placeholder;
const onError = ownOnError ?? provided?.onError;
const options = useMergedOptions({
...(registry !== undefined ? { registry } : {}),
...(catalog !== undefined ? { catalog } : {}),
...(resolveComponent !== undefined ? { resolveComponent } : {}),
...(treeRenderer !== undefined ? { treeRenderer } : {}),
});
const state = useGenerativeUIState(part, options, options.treeRenderer ?? GenuiTree);
if (state === null)
return null;
if (state.status === 'loading')
return loading;
if (state.status === 'problem') {
const { status: _status, ...problem } = state;
return renderFallback(fallback, problem);
}
const Renderer = state.Component;
return (_jsx(GenerativeUIScope, { ...options, ...(state.item.componentVersions !== undefined
? { componentVersions: state.item.componentVersions }
: {}), ...(state.item.fallbackText !== undefined ? { fallbackText: state.item.fallbackText } : {}), fallback: fallback, loading: loading, ...(placeholder !== undefined ? { placeholder } : {}), ...(onError !== undefined ? { onError } : {}), children: _jsx(ItemBoundary, { item: state.item, fallback: fallback, onError: onError, children: _jsx(Renderer, { ...state.props }) }) }));
}
//# sourceMappingURL=generative-ui.js.map