UNPKG

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