UNPKG

@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
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