@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
284 lines • 15.3 kB
TypeScript
/**
* Per-channel generative UI: what a turn may draw depends on where it is read. A web page draws the
* catalog's components (and the sandbox); WhatsApp and Telegram draw text, reply buttons, lists and
* images; an email draws HTML. `genui({ channels: { … } })` sets, per channel, which tools the model
* is offered and how what it pushes is delivered — and the model is told only what that channel can
* draw.
*
* Framework-free and isomorphic, like the rest of `genui`.
*/
import type { UiAction } from './actions.js';
import { type Catalog, type ComponentDefinition, type GenuiStreaming } from './catalog.js';
import { type DefineSandboxOptions } from './sandbox.js';
/** The channel a turn runs on when nothing says otherwise: a browser (AG-UI, the HTTP chat, A2UI). */
export declare const WEB_CHANNEL = "web";
/** The `channels` entry that serves every channel without one of its own. */
export declare const DEFAULT_CHANNEL = "default";
/** Channels a person reads in a messaging app or a mailbox: the server draws for them. */
export declare const MESSAGING_CHANNELS: readonly string[];
/**
* What the model is offered on a channel: `tree` — one `ui__render` tool composing the catalog (and
* the sandbox, when on); `per-component` — one `ui__show_<component>` tool per component that can be
* drawn there; `text` — no UI tools, the model answers in text.
*/
export type GenuiChannelMode = 'tree' | 'per-component' | 'text';
/**
* How what the model pushed reaches a channel the server draws for (WhatsApp, Telegram, email):
* `native` — each component's channel conversion (`defineComponent(…, { channels })`: text, reply
* buttons, a list, an image), its text summary otherwise; `text` — always the text summary; `html` —
* HTML (email). A web channel draws in the browser and ignores it.
*/
export type GenuiChannelRender = 'native' | 'text' | 'html';
/** One channel's generative UI — every field falls back to the `default` entry, then to the top level. */
export interface GenuiChannelOptions {
/** Default: the top-level `mode` on a web channel; `per-component` on a messaging channel. */
mode?: GenuiChannelMode;
/** Tree mode: draw the layout while the model writes it (`partial`) or once it is whole. */
streaming?: GenuiStreaming;
/**
* The sandbox on this channel: `false` for none, `true` / a config for the sandbox with those
* options (a web channel may use the app's kit while a mobile one does not). Default: the
* top-level `sandbox` on a web channel; never on a messaging channel (it cannot run there).
*/
sandbox?: boolean | DefineSandboxOptions;
/** Default `native` (`html` for `email`). */
render?: GenuiChannelRender;
/** Charts as PNG images on a channel that takes images ({@link ChartImageRenderer}, `chartImages()`). */
chartImages?: ChartImageRenderer;
/** Extra words for the model on this channel, added to the UI tools' descriptions. */
instructions?: string;
}
/** `genui({ channels })`: an entry per channel name, plus `default` for every other one. */
export interface GenuiChannels {
web?: GenuiChannelOptions;
mobile?: GenuiChannelOptions;
whatsapp?: GenuiChannelOptions;
telegram?: GenuiChannelOptions;
email?: GenuiChannelOptions;
default?: GenuiChannelOptions;
[channel: string]: GenuiChannelOptions | undefined;
}
/** The top-level genui options a channel falls back to. */
export interface GenuiChannelBase {
mode?: 'per-component' | 'tree';
streaming?: GenuiStreaming;
sandbox?: boolean | DefineSandboxOptions;
}
/** A channel's options with every default applied — what the tools and the channel handler follow. */
export interface ResolvedGenuiChannel {
/** The channel's name (`web`, `whatsapp`, `mobile`, …). */
name: string;
/** The server draws for it (WhatsApp, Telegram, email): it never runs a sandbox or a React renderer. */
messaging: boolean;
/** `channels` has an entry for it (its own or `default`). `false` → it behaves as it always did. */
configured: boolean;
mode: GenuiChannelMode;
streaming?: GenuiStreaming;
/** `undefined` → the catalog's own sandbox (if it has one) is left as it is. */
sandbox?: boolean | DefineSandboxOptions;
render: GenuiChannelRender;
chartImages?: ChartImageRenderer;
instructions?: string;
}
/**
* THE rule for which channel a turn runs on, read from its page context:
*
* - `pageContext.channel` is a string → that channel (an explicit override: `send({ channel })`, a
* mobile client posting `pageContext: { channel: 'mobile' }`);
* - `pageContext.channel` is the address a text-channel adapter stamps (`{ name, conversation,
* kind? }`) → its `kind` (`whatsapp`, `telegram`), else its `name`;
* - nothing → `web`: what AG-UI, the HTTP chat and A2UI serve (they stamp nothing, so a tool that
* skips skeletons when `pageContext.channel` is set keeps working).
*/
export declare function turnChannel(pageContext: {
[key: string]: unknown;
} | undefined): string;
/** WhatsApp, Telegram and email are drawn by the server; every other channel by a client. */
export declare function isMessagingChannel(channel: string): boolean;
/** One channel's options, its own entry over `default` over the top level. */
export declare function resolveGenuiChannel(base: GenuiChannelBase, channels: GenuiChannels | undefined, channel: string | undefined): ResolvedGenuiChannel;
/** Throws on a `channels` entry that names an unknown mode or render. */
export declare function assertGenuiChannels(channels: GenuiChannels | undefined): void;
/**
* A reply button. Pressed, it becomes the user's next turn — a {@link UiAction} whose text is the
* label and whose context is `value`, exactly like a sandbox's `agent.send`.
*/
export interface ChannelNativeButton {
label: string;
/** The action's name. Default `select`. */
action?: string;
/** The values that go with the press (JSON). */
value?: Record<string, unknown>;
}
export interface ChannelNativeListItem extends ChannelNativeButton {
/** A second line under the label. */
description?: string;
}
/** A list to pick one entry from (a WhatsApp list message, a Telegram keyboard of one row each). */
export interface ChannelNativeList {
/** The label of the button that opens the list (WhatsApp). */
button: string;
/** A heading over the entries. */
title?: string;
items: ChannelNativeListItem[];
}
/** An image: by an `https` url the provider fetches, or its bytes. */
export interface ChannelNativeImage {
url?: string;
data?: Uint8Array;
/** Default `image/png`. */
contentType?: string;
}
/**
* What a component is on a channel the server draws for. Each field is optional; a message with an
* image sends `text` as its caption, one with buttons or a list sends `text` as its body.
*/
export interface ChannelNativeMessage {
/** In the model's markdown — converted to the channel's. */
text?: string;
/** For `render: 'html'` (email). Trusted markup: build it from props, escaping what you insert. */
html?: string;
/** Reply buttons — at most 3 on WhatsApp; more become a list. */
buttons?: ChannelNativeButton[];
list?: ChannelNativeList;
image?: ChannelNativeImage;
}
/**
* What a conversion is told about the frame it converts. `version` is the schema version the props
* were written for (the frame's — a v1 frame persisted before the component moved to v2 still says
* 1); convert the props by it, as a renderer would.
*/
export interface ChannelConversionContext {
/** The channel (`whatsapp`, `telegram`, `email`…). */
channel: string;
/** The component being converted (inside a tree: the node's). */
component: string;
/** The props' schema version: the frame's, or the tree's `componentVersions` entry. Default 1. */
version: number;
/** The frame's id, when it has one. */
id?: string;
/** The whole frame the component came from (a tree's, for one of its nodes), as given. */
frame?: ChannelComponentInput;
}
/** What `describe` is told: the channel, and the version the component is at now. */
export interface ChannelDescribeContext {
channel: string;
component: string;
/** The component definition's current version (default 1). */
version: number;
}
/** `defineComponent(…, { channels: { whatsapp: (props, { version }) => ({ text, buttons }) } })`. */
export type ChannelConversion<P = Record<string, unknown>> = {
convert(props: P, context: ChannelConversionContext): ChannelNativeMessage | ChannelNativeMessage[] | null | undefined | Promise<ChannelNativeMessage | ChannelNativeMessage[] | null | undefined>;
}['convert'];
/**
* A conversion with words for the model: `describe` says how the component reaches the user on that
* channel — `'a pick-list of the orders; no Refund buttons: to refund, the user picks an order and
* replies'` — and goes into the channel's prompt (`channelPrompt`), so the model does not describe the
* web view there. A bare conversion is described from what it can send.
*/
export interface ChannelConversionSpec<P = Record<string, unknown>> {
convert: ChannelConversion<P>;
/** Words for the model, or a function of the channel and the component's current version. */
describe?: string | ((context: ChannelDescribeContext) => string | undefined);
}
/**
* A component's conversions, by channel name — a function, or `{ convert, describe }`. `false` →
* the component is never offered on that channel (it has nothing to say there).
*/
export type ComponentChannels<P = Record<string, unknown>> = {
[channel: string]: ChannelConversion<P> | ChannelConversionSpec<P> | false | undefined;
};
/** Renders chart components as images, for a channel that takes them. */
export interface ChartImageRenderer {
/** The components it draws (default for `chartImages()`: `['Chart']`). */
readonly components: readonly string[];
render(component: string, props: Record<string, unknown>): Promise<ChannelNativeImage | null>;
}
/**
* Whether `definition` can be drawn on `channel`. On a web channel every component can (the client's
* `uiCapabilities` narrow it); on a messaging channel a component can when it has a conversion for
* it, a chart renderer draws it, or it has a text summary (`fallbackText`) — a component without one
* would arrive as a JSON dump, so it is not offered. A sandbox never can, and `channels: { x: false }`
* opts a component out.
*/
export declare function canRenderOnChannel(definition: ComponentDefinition<unknown>, channel: ResolvedGenuiChannel): boolean;
/**
* The catalog as `channel` may draw it: only what it can render, and its own sandbox (or none). With
* no `channels` configured the catalog is returned as it is.
*/
export declare function channelCatalog(catalog: Catalog, channel: ResolvedGenuiChannel): Catalog;
/** What a UI tool's description says about the channel the conversation is on. */
export declare function channelInstructions(channel: ResolvedGenuiChannel): string | undefined;
/** A pushed component, as a channel delivers it. */
export interface ChannelComponentInput {
/** The frame's id — what a button press reports as its `componentId`. */
id?: string;
name: string;
props: Record<string, unknown>;
/** The schema version the props were written for (the frame's `version`). Default 1. */
version?: number;
/** A tree frame's per-component versions (`componentVersions`), for its nodes. */
componentVersions?: Record<string, number>;
/**
* The frame's own text (`fallbackText`): what is sent for a component whose props were written
* for a schema version this catalog does not have — it is never drawn from them as if they were.
*/
fallbackText?: string;
}
/** {@link ChannelNativeMessage} plus the component it came from (so a press can name it). */
export interface ChannelRenderedMessage extends ChannelNativeMessage {
component: string;
componentId?: string;
title?: string;
}
/** Plain text as minimal HTML: paragraphs, line breaks, `*bold*`. */
export declare function textToHtml(text: string): string;
/**
* What a pushed component (a tree included) is on a channel the server draws for: one or more
* messages — its conversions, chart images, and the text summary of everything else, in order.
* Consecutive text is joined. Never throws: a conversion that fails sends the summary.
*/
export declare function renderChannelMessages(catalog: Catalog, component: ChannelComponentInput, channel: ResolvedGenuiChannel): Promise<ChannelRenderedMessage[]>;
/** The UI action a pressed button (or picked list entry) stands for — the user's next turn. */
export declare function channelButtonAction(button: ChannelNativeButton, from: {
componentId?: string;
title?: string;
}): UiAction;
/**
* The system-prompt block for a turn on a messaging channel (WhatsApp, Telegram, email): that the
* user is not on a web page, what the channel can show, and — component by component — how each one
* reaches the user there, from its channel conversion (its `describe`, else what a conversion can
* send), a chart image, or its text summary. Tool and component descriptions are written for the web
* ("a table with a Refund button per row"); this is what keeps the model from describing that view
* on WhatsApp. `undefined` on a web-like channel, or without `channels`.
*/
export declare function channelPrompt(catalog: Catalog, channel: ResolvedGenuiChannel): string | undefined;
/**
* How many reply buttons and list rows a channel shows — the adapter's `capabilities.buttons` /
* `capabilities.lists`, which the channel handler stamps on the turn (`pageContext.channel.choices`).
*/
export interface ChannelChoiceLimits {
buttons: number;
lists: number;
}
/**
* What a channel actually delivered, in words for the model: each message (text, a pick-list and its
* entries, reply buttons, numbered choices, an image, an email section), and what the user did NOT
* get. This is what a tool's result carries as `delivered` on a messaging channel, so the model talks
* about the list the user can pick from — not the table with Refund buttons it would have on the web.
*
* Choices follow the channel handler's rule, within `limits` (default: the channel's own adapter's):
* reply buttons when there is no list (or no lists) and they fit, else a list of at most `lists`
* rows, else numbered text the user answers by replying.
*/
export declare function describeChannelDelivery(messages: readonly ChannelRenderedMessage[], channel: ResolvedGenuiChannel, limits?: ChannelChoiceLimits): string;
/**
* {@link describeChannelDelivery} of the components a tool pushed (last props per id, previews left
* out), rendered the way the channel draws them. `undefined` on a channel the server does not draw
* for, or without `channels`.
*/
export declare function describeChannelComponents(catalog: Catalog, components: readonly (ChannelComponentInput & {
partial?: boolean;
})[], channel: ResolvedGenuiChannel, limits?: ChannelChoiceLimits): Promise<string | undefined>;
//# sourceMappingURL=channels.d.ts.map