@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
264 lines • 11.6 kB
TypeScript
import type { UIMessage } from 'ai';
import type React from 'react';
import type { QueuePause } from '../../../index.js';
import type { ApprovalTarget } from '../approvals/proposals.js';
import { type MessageFile } from '../attachments/files.js';
import type { ToolCatalog } from '../presentation/phrasing.js';
import type { QueuedChatMessage } from '../queue/model.js';
import { type ApproveOptions, type ChatStatus, type MessageUsageInfo, type TimestampInfo, type TranscriptBlock, type TranscriptFile, type TranscriptUiAction, type UsageSummary } from './model.js';
import { type StickToBottom } from './use-stick-to-bottom.js';
export interface TranscriptCopyState {
/** False when the message has no prose to put on the clipboard. */
available: boolean;
copied: boolean;
copy: () => void;
}
export interface TranscriptEditState {
available: boolean;
isEditing: boolean;
draft: string;
/** Whitespace-only drafts are refused, so an empty resubmit can never be sent. */
canSave: boolean;
start: () => void;
cancel: () => void;
setDraft: (value: string) => void;
save: () => void;
/** Controlled-textarea wiring: focus with the caret at the end, Enter saves, Escape cancels. */
getTextareaProps: () => {
ref: (element: HTMLTextAreaElement | null) => void;
value: string;
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
};
}
export interface TranscriptActionState {
available: boolean;
run: () => void;
}
export interface TranscriptItem {
id: string;
message: UIMessage;
role: UIMessage['role'];
/** Index in the FULL message array, not in the rendered window. */
index: number;
isUser: boolean;
isAssistant: boolean;
isLastAssistant: boolean;
isStreaming: boolean;
blocks: TranscriptBlock[];
/** The prose alone — what `copy` puts on the clipboard and what an edit starts from. */
text: string;
/**
* A user message that is a UI action: its parts (see `TranscriptTextBlock.uiAction`). `null` on
* every other message.
*/
uiAction: TranscriptUiAction | null;
usage: UsageSummary | null;
timestamp: TimestampInfo | null;
copy: TranscriptCopyState;
edit: TranscriptEditState;
fork: TranscriptActionState;
regenerate: TranscriptActionState;
}
/** What an edit-and-resubmit sends. */
export interface EditSubmitInput {
messageId: string;
text: string;
}
/** Names the message an action is about. */
export interface MessageActionInput {
messageId: string;
}
/** Settling a parked question set. `answers` holds only the questions the user touched. */
export interface AnswerInput {
toolCallId: string;
answers?: Record<string, string[]>;
/** The surface the answer came through; the server records `'web'` when omitted. */
via?: string;
}
/** Declining a parked question set. */
export interface SkipInput {
toolCallId: string;
/** The surface the skip came through; the server records `'web'` when omitted. */
via?: string;
}
/** Approving a tool call parked on a human. */
export interface ApproveInput extends ApproveOptions {
target?: ApprovalTarget;
toolCallId: string;
/** The surface the decision came through; the server records `'web'` when omitted. */
via?: string;
}
/** Rejecting a tool call parked on a human. */
export interface RejectInput {
target?: ApprovalTarget;
toolCallId: string;
reason?: string;
via?: string;
}
export interface TranscriptItemOptions {
/**
* A transcript nobody acts on (an audit view, a shared or archived conversation): no approve /
* reject, answer / skip, edit, fork, regenerate or stop — whatever handlers or backend are in
* scope. Parked approvals and question sets still render, with their actions unavailable.
*
* Without it the decision handlers DEFAULT to the in-scope backend (`<AgentProvider>`'s), so a
* parked call is actionable with no wiring; a read-only surface would otherwise have to pass
* `onApprove: null`, `onReject: null`, `onAnswer: null` and `onSkip: null` one by one.
*/
readOnly?: boolean;
/** User messages get an inline edit-and-resubmit machine. */
editable?: boolean;
onEditSubmit?: (input: EditSubmitInput) => void | Promise<void>;
onFork?: (input: MessageActionInput) => void | Promise<void>;
/** Only the LAST assistant message gets a regenerate machine. */
regeneratable?: boolean;
onRegenerate?: (input: MessageActionInput) => void | Promise<void>;
/** Default: `metadata.usage` (replayed turns; see `AgentMessageMetadata`). */
getUsage?: (message: UIMessage) => MessageUsageInfo | null;
/** Default: `metadata.createdAt` (replayed and streamed messages). */
getCreatedAt?: (message: UIMessage) => string | null;
/** How long `copy.copied` stays true. Default 1500ms. */
copyResetMs?: number;
/** Clipboard writer; defaults to `navigator.clipboard.writeText`. */
writeClipboard?: (text: string) => Promise<void>;
/**
* Lift retrieval tool parts into their own `sources` block instead of leaving them in the tool
* run. Off by default, so a renderer already drawing tool cards keeps drawing them.
*/
sources?: boolean;
/**
* Server-declared tool presentations (`useToolCatalog(...).catalog`). Given → each tool call's
* `description` and each tool block's `activity` speak in the tool's own words; omitted → generic.
*/
toolCatalog?: ToolCatalog;
/**
* Leave out a failed tool call the model retried at once with the same tool (`retriedCallIds`).
* Default `true`; `false` keeps every attempt.
*/
hideRetriedFailures?: boolean;
/**
* Settle a parked question set. Default: the in-scope backend's `answerToolCall` (the enclosing
* `<AgentProvider>`'s), so a question set is an `elicitation` block the user can act on without
* any wiring. `null` turns question sets back into plain tool calls.
*
* `answers` holds ONLY the questions the user touched. An omitted one takes its own pre-picked
* default server-side, which is what keeps "just confirmed" and "picked exactly what was
* pre-picked" distinguishable in the settled row.
*/
onAnswer?: ((input: AnswerInput) => void | Promise<void>) | null;
/** Decline the question set and let the agent proceed on its own picks. Default: the backend's. */
onSkip?: ((input: SkipInput) => void | Promise<void>) | null;
/**
* Settle a tool call parked on a human. Default: the in-scope backend's `approveToolCall` /
* `rejectToolCall`. `remember` is what `call.approve.run({ remember: true })` passed. `null`
* leaves the call without the affordance.
*/
onApprove?: ((input: ApproveInput) => void | Promise<void>) | null;
onReject?: ((input: RejectInput) => void | Promise<void>) | null;
}
export interface TranscriptWindow {
visibleCount: number;
/** How many older messages are not mounted. */
hiddenCount: number;
canLoadEarlier: boolean;
loadEarlier: () => void;
showAll: () => void;
}
export interface TranscriptStopState {
available: boolean;
/** True from the click until the turn actually leaves a busy status. */
isStopping: boolean;
stop: () => void;
}
export interface UseChatTranscriptOptions extends TranscriptItemOptions {
messages: UIMessage[];
status: ChatStatus;
initialVisibleCount?: number;
loadEarlierStep?: number;
/** Cancel the in-flight turn — wire to `useAgentChat`'s `cancel`. */
onStop?: () => void | Promise<void>;
/** Suggested follow-ups. The model only decides WHETHER they belong on screen. */
followUps?: string[] | null;
stickToBottomThreshold?: number;
/**
* Messages waiting in the thread's queue — `chat.queue` (`useAgentChat` wires it). They render
* after the transcript as {@link ChatTranscript.queued}: pending user messages, not yet sent.
*/
queue?: {
/** `files` may be left out by a host that builds these itself: it is derived from `attachments`. */
items: Array<Omit<QueuedChatMessage, 'files'> & {
files?: MessageFile[];
}>;
paused: QueuePause | null;
remove?: (id: string) => void | Promise<void>;
} | null;
}
/** One file on a waiting message: a sent message's {@link MessageFile}, and a `TranscriptFile`. */
export type TranscriptQueuedFile = MessageFile & TranscriptFile;
/**
* A message waiting in the thread's queue, as the transcript shows it: a user message that has not
* been sent to the model yet. Render it after {@link ChatTranscript.items}, dimmed or tagged.
*/
export interface TranscriptQueuedItem {
id: string;
role: 'user';
text: string;
/**
* The message's files, in the shape a sent message's have: a {@link MessageFile} (what
* `messageFiles()` gives — `kind`, `extension`, `mediaId`) that is also a `TranscriptFile`
* (`isImage`), so either renderer takes it as it is.
*/
files: TranscriptQueuedFile[];
/**
* `'sending'` — on its way to the server. `'queued'` — waiting for the running turn.
* `'paused'` — waiting, but the queue is paused (see {@link ChatTranscript.queuePaused}).
*/
state: 'sending' | 'queued' | 'paused';
/** 0-based place in the queue. */
position: number;
/** Runs next. */
isNext: boolean;
/** Queued by an interrupt — it runs as soon as the cancelled turn settles. */
interrupt: boolean;
remove: TranscriptActionState;
}
export interface ChatTranscript {
/** The mounted window, oldest first. `window.hiddenCount` says what is held back. */
items: TranscriptItem[];
status: ChatStatus;
isEmpty: boolean;
/** A turn is in flight: sent but not settled. */
isBusy: boolean;
isStreaming: boolean;
showEmptyState: boolean;
/** Waiting on the first token of a turn that has no assistant message yet. */
showTypingIndicator: boolean;
showFollowUps: boolean;
lastAssistantId: string | null;
window: TranscriptWindow;
scroll: StickToBottom;
stop: TranscriptStopState;
/** Messages waiting in the thread's queue, in run order — render after `items`. */
queued: TranscriptQueuedItem[];
/** Why the queue is not draining, or `null`. */
queuePaused: QueuePause | null;
}
/**
* The model behind a chat transcript: messages normalized into items with their parts grouped into
* runs, per-message derived values (copyable text, usage labels, relative time), each action as a
* state machine rather than a button, list windowing, and stick-to-bottom. It renders nothing and
* names no class — `MessageList`/`MessageItem` are one renderer over it, and an app with its own
* design system writes another without reimplementing any of this.
*/
export declare function useChatTranscript(options: UseChatTranscriptOptions): ChatTranscript;
export interface UseTranscriptItemOptions extends TranscriptItemOptions {
message: UIMessage;
isStreaming?: boolean;
}
/**
* The single-message half of {@link useChatTranscript}, for a renderer that owns the list itself.
* `regeneratable` applies directly here: one assistant message is trivially the last one.
*/
export declare function useTranscriptItem(options: UseTranscriptItemOptions): TranscriptItem;
//# sourceMappingURL=use-chat-transcript.d.ts.map