@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
253 lines • 11.1 kB
TypeScript
/**
* The contract between a text channel (WhatsApp, Telegram, …) and the agent: what an adapter reads
* off a webhook and what it is asked to deliver. Framework-free — `channels.handle()` builds the
* {@link ChannelRequest} from Adonis's `HttpContext`, and a test builds one by hand.
*/
/** The markdown a channel renders: WhatsApp's, Telegram's MarkdownV2, or none (plain text). */
export type ChannelMarkdown = 'whatsapp' | 'telegram' | 'none';
/** What a channel can show — what the service shapes every reply to. */
export interface ChannelCapabilities {
/**
* The most reply buttons one message can carry. Omitted (or below 2) → the channel has none, and a
* proposal is put to the person as a text instruction instead of Confirm/Cancel buttons.
*/
buttons?: number;
/** The markdown dialect replies are converted to. */
markdown: ChannelMarkdown;
/** The longest text one message may carry; longer replies are split into several messages. */
maxLength: number;
/**
* The adapter can send files ({@link OutboundMessage} with `media`) — images and documents with a
* caption. Omitted → a file reply is sent as its caption / fallback text.
*/
media?: boolean;
/** The longest caption a file may carry. Default: {@link maxLength}. */
maxCaptionLength?: number;
/**
* The most entries a list message can carry (a WhatsApp list, a Telegram keyboard of one row per
* entry). Omitted → no lists: a component's list goes out as reply buttons when it fits, else as
* numbered text.
*/
lists?: number;
}
/** One webhook request, as an adapter sees it. */
export interface ChannelRequest {
/** Upper-case HTTP method. */
method: string;
/** Path and query string, e.g. `/webhooks/whatsapp?hub.mode=subscribe`. */
url: string;
/** A request header by (case-insensitive) name. */
header(name: string): string | undefined;
/** Route params (`/webhooks/whatsapp/:token` → `{ token }`). */
params: Record<string, unknown>;
/** The parsed body. */
body: unknown;
/**
* The body exactly as it arrived — what a signature is computed over. `null` when the body parser
* did not keep it (a request it does not parse).
*/
rawBody: string | null;
}
/** A message a person sent on the channel. */
export interface InboundMessage {
/** The provider's id for this message — retries of one delivery carry the same id. */
id: string;
/** Who sent it: a phone number, a Telegram user id… — what `actor()` maps to an account. */
from: string;
/** Where replies go: the chat / conversation id {@link ChannelAdapter.send} is called with. */
conversation: string;
/**
* What the person wrote — for a button press, the button's label (or its id when there is none);
* for a media message, its caption (`''` when it has none).
*/
text: string;
/** Files the message carries (an image, a voice note, a document) — downloaded and attached to the turn. */
media?: InboundMedia[];
/** The id of the reply button the person pressed, when the message is a button press. */
buttonId?: string;
/**
* A button press whose id the provider did not forward — only its label, in {@link text}
* (Whatsmiau). The handler maps it to the proposal card it can only have come from, if there is
* exactly one.
*/
buttonWithoutId?: boolean;
/**
* When the person sent it, in epoch milliseconds — the provider's own timestamp (Evolution /
* Whatsmiau `messageTimestamp`, WhatsApp Cloud `timestamp`, Telegram `date`). Stored with the turn's
* message (`StoredMessage.sentAt`), so a turn that waited (a queue, a `coalesce` burst) still knows
* when it was said — what `messageTimestamps` shows the model. Absent when the provider sent none
* (a Telegram button press): the message's `createdAt` stands for it.
*/
sentAt?: number;
/** The provider's payload, untouched. */
raw: unknown;
}
/** One file in an inbound message, before it is downloaded. */
export interface InboundMedia {
kind: 'image' | 'audio' | 'video' | 'document' | 'sticker';
/** MIME type, when the webhook says (it usually does). */
contentType?: string;
/** The file's name, when the webhook says (documents). */
filename?: string;
/** Size in bytes, when the webhook says. */
sizeBytes?: number;
/** Whatever the adapter needs to download it (a media id, a file id, the message key). */
ref: unknown;
}
/** A downloaded file. */
export interface ChannelMediaFile {
data: Buffer;
contentType: string;
filename?: string;
}
export interface ChannelButton {
/** What comes back as {@link InboundMessage.buttonId} when it is pressed. */
id: string;
label: string;
}
/** One entry of a list message. */
export interface ChannelListRow {
/** What comes back as {@link InboundMessage.buttonId} when it is picked. */
id: string;
title: string;
description?: string;
}
/** A list to pick one entry from — see {@link ChannelCapabilities.lists}. */
export interface OutboundList {
/** The label of the button that opens the list (WhatsApp). */
button: string;
/** A heading over the entries. */
title?: string;
rows: ChannelListRow[];
}
/** A file to send: by a URL the provider downloads it from, or by its bytes. */
export interface OutboundMedia {
kind: 'image' | 'document' | 'audio' | 'video';
/** An `https` URL the provider fetches the file from (short-lived URLs are fine). */
url?: string;
/** The file itself, when there is no URL. One of `url` / `data` is required. */
data?: Buffer;
/** MIME type — `image/png`, `application/pdf`. Recommended (some providers require it). */
contentType?: string;
/** The name a document is shown under. */
filename?: string;
}
/**
* One message to deliver: text, text with reply buttons, or a file with a caption (`text`, `''` for
* none). An adapter without {@link ChannelCapabilities.media} is never handed a file: the handler
* sends the caption (or the reply's `fallbackText`) instead.
*/
export type OutboundMessage = {
text: string;
buttons?: undefined;
fallbackText?: undefined;
instruction?: undefined;
footer?: undefined;
media?: undefined;
list?: undefined;
} | {
text: string;
buttons: ChannelButton[];
/**
* The same message as text only — a reply instruction in place of the buttons. What an adapter
* sends when the provider definitely refused the buttons (a 4xx), so the person still gets a
* way to answer.
*/
fallbackText: string;
/**
* The reply instruction alone ("Reply *yes* to confirm or *no* to cancel."), already in the
* channel's markdown. An adapter whose buttons may not render on every phone puts it in the
* buttons message too, so the person can still answer by text.
*/
instruction?: string;
/**
* A short line under the card (`texts.footer` — "Valid for 5 minutes."), already in the
* channel's markdown. Sent as the provider's footer where it has one, else after the text.
* `fallbackText` already ends with it.
*/
footer?: string;
media?: undefined;
list?: undefined;
} | {
/** The body over the list, already in the channel's markdown. */
text: string;
list: OutboundList;
/** The same message as numbered text — what an adapter sends when the list is refused. */
fallbackText: string;
buttons?: undefined;
instruction?: undefined;
footer?: undefined;
media?: undefined;
} | {
/** The caption, already in the channel's markdown — `''` for none. */
text: string;
media: OutboundMedia;
buttons?: undefined;
fallbackText?: undefined;
instruction?: undefined;
footer?: undefined;
list?: undefined;
};
/** A reply the adapter answers the webhook with instead of acknowledging it (a verification GET). */
export interface ChannelChallengeResponse {
status: number;
body: string;
contentType?: string;
}
/** Why a webhook body carried no message to answer — see {@link ChannelAdapter.ignored}. */
export interface ChannelIgnored {
/** The provider's event name, when the body has one. */
event?: string;
/** Short and content-free: `own message`, `group`, `fromMe missing and status PENDING`… */
reason: string;
/** It looked like a person's message but could not be read — logged as a warning, not debug. */
unexpected?: boolean;
}
/**
* One text channel. The built-in ones are `evolutionApi()`, `whatsmiau()`, `whatsappCloud()` and
* `telegram()`; any object of this shape works the same way.
*/
export interface ChannelAdapter {
/** Names the channel: the dedupe key's prefix, and `via` on a decision made with its buttons. */
readonly name: string;
/**
* What the channel is — `whatsapp`, `telegram` — whatever it is named: the turn's channel
* (`turnChannel`), so `genui({ channels: { whatsapp } })` applies to every WhatsApp adapter.
* Default: {@link name}.
*/
readonly kind?: string;
readonly capabilities: ChannelCapabilities;
/**
* Answer a request that is not a message — WhatsApp Cloud's `GET ?hub.challenge=…` subscription
* check. `null` → not one of those; the request goes on to {@link verify}.
*/
challenge?(request: ChannelRequest): ChannelChallengeResponse | null;
/** Is this request really from the provider (a signature, a shared token)? `false` → `401`. */
verify(request: ChannelRequest): boolean | Promise<boolean>;
/**
* The person's message(s) in a verified webhook body. `null` (or `[]`) → nothing to answer: a
* delivery receipt, the bot's own message, a group, a media message…
*/
parse(body: unknown): InboundMessage | InboundMessage[] | null;
/**
* Why a body {@link parse}d to no message — the handler logs it, so a dropped webhook can be
* diagnosed. Called only then; never put message content in it. `null` → no reason to give.
*/
ignored?(body: unknown): ChannelIgnored | null;
/**
* Tell the provider the message was received, where it waits for that — Telegram's
* `answerCallbackQuery` after a button press. Runs in the background, before the turn.
*/
acknowledge?(message: InboundMessage): Promise<void>;
/**
* Download one of a message's {@link InboundMessage.media}. Throws {@link
* import('./http.js').ChannelMediaTooLargeError} past `maxBytes` (without reading the rest).
* Absent → media messages are answered with `texts.mediaRefused`.
*/
download?(media: InboundMedia, options: {
maxBytes: number;
}): Promise<ChannelMediaFile>;
/** Deliver one message to a conversation. Throws when the provider refused it. */
send(conversation: string, message: OutboundMessage): Promise<void>;
}
//# sourceMappingURL=types.d.ts.map