UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

419 lines • 20.1 kB
import { type DynamicToolUIPart, type ToolUIPart, type UIMessage } from 'ai'; import { type ElicitationInput } from '../../../elicitation-input.js'; import { type UiActionMessage } from '../../../genui/actions.js'; import type { ToolConfirmation } from '../../../index.js'; import type { ApprovalTarget } from '../approvals/proposals.js'; import { type RawAnswer } from '../elicitation/answers.js'; import type { ToolCatalog } from '../presentation/phrasing.js'; import { type ToolActivityGroup, type ToolCallDescription } from '../presentation/tool-activity.js'; /** A tool UI part on a `UIMessage` — a static `tool-*` part or the `dynamic-tool` part. */ export type AnyToolUIPart = ToolUIPart | DynamicToolUIPart; /** * The AI SDK's chat status, plus `reconnecting`: a turn is in flight and its stream dropped, and the * transport is re-attaching (`useAgentChat`). A busy status, like `streaming`. */ export type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'reconnecting' | 'error'; /** Server-aggregated usage for an assistant turn. */ export interface MessageUsageInfo { inputTokens: number; outputTokens: number; /** * `null` when no price is on record for the model that ran the turn. Distinct from `0`, which is * a turn that genuinely cost nothing — printing `$0` for an unpriced turn states a number the * store never had. */ costUsd: number | null; } /** A run of contiguous prose. `isStreaming` is the PART's own state, not the message's. */ export interface TranscriptTextBlock { kind: 'text'; key: string; text: string; isStreaming: boolean; /** * A user message that is a UI action (a sandbox's `agent.send`, a component's button — * `uiActionText`): its parts. Draw `uiAction.text` (or `uiActionSummary(uiAction)`) as a chip * instead of `text`, whose JSON block is for the model. Absent on every other block. */ uiAction?: TranscriptUiAction; } /** A UI action a user message carries ({@link TranscriptTextBlock.uiAction}). */ export type TranscriptUiAction = UiActionMessage; /** One file on a message — an uploaded attachment, or one the model produced. */ export interface TranscriptFile { /** Presigned or otherwise directly fetchable. Display-only: the model reads its own copy. */ url: string; mediaType: string; filename: string | null; /** `image/*`, which a renderer can show inline rather than as a link. */ isImage: boolean; } /** A run of contiguous files. Grouped so a renderer can lay several out as one strip. */ export interface TranscriptFilesBlock { kind: 'files'; key: string; files: TranscriptFile[]; } /** * A run of contiguous reasoning. Carries its own disclosure state because a thread can hold many * of them and each is toggled independently; `isOpen` defaults to `isStreaming` (thinking is worth * watching live, worth folding away once answered) until `toggle` is called for that run. */ export interface TranscriptReasoningBlock { kind: 'reasoning'; key: string; text: string; isStreaming: boolean; /** * How long the model thought, in ms — the backend's measurement once the run closed (live) or as * persisted (reloaded). `null` while it still streams, or when nothing was recorded: pair it with * `useElapsed(block.isStreaming)` for a ticking label (`block.durationMs ?? elapsed`). */ durationMs: number | null; isOpen: boolean; toggle: (open?: boolean) => void; } /** * A decision a run is waiting on a human for, and which one a parked call is currently sending. * * One call settles one way at a time, and WHICH one is what lets a surface report progress on the * affordance the person pressed instead of on all of them. */ export type SettleAction = 'approve' | 'reject' | 'answer' | 'skip'; /** One such decision: whether it can be made, whether it is on its way, and how to send it. */ export interface TranscriptSettleState { available: boolean; /** True from the click until the run resumes and settles the call. */ isSubmitting: boolean; run: () => void; } /** How an approval stands. */ export type TranscriptApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired' | 'superseded'; /** * Who has to settle a parked call, until when, and — once it settled — how. From the stream's * `approval-requested` / `approval-settled` frames, or the same parts a reloaded thread carries. * `null` on a call whose runner never said (the call can still be awaiting approval). */ export interface TranscriptApproval { target?: ApprovalTarget; executionStatus?: 'queued' | 'executing' | 'succeeded' | 'failed' | null; confirmation?: ToolConfirmation; /** Open vocabulary the host defines: `'requester'`, `'admin'`, a role… */ approver: string; /** ISO-8601; `null` when the request never lapses. Pair with `useApprovalCountdown`. */ expiresAt: string | null; /** Why the call needs a person, when the runner said. */ reason: string | null; /** * `pending` until a settlement arrives; then what it said. Falls back to the call's own state * (denied → `rejected`, an output → `approved`) for a runner that streams no settlement. */ status: TranscriptApprovalStatus; /** The approval also covers later calls of this tool in this thread. */ remember: boolean; /** Opaque ref of who decided; `null` while pending, on an expiry, or when the runner did not say. */ decidedBy: string | null; /** The surface the decision came through (`'web'`, `'slack'`, `'remembered'`, …). */ decidedVia: string | null; /** What the person said when declining. */ decisionReason: string | null; } /** What an approval can carry beyond yes. */ export interface ApproveOptions { /** Approve later calls of the same tool in the same thread without asking again. */ remember?: boolean; } /** One call in a tool run, with whatever human decision it is parked on. */ export interface TranscriptToolCall { part: AnyToolUIPart; toolCallId: string; name: string; /** `read` / `action` as the stream classified the call; `null` when the backend did not say. */ toolKind: string | null; /** The call this one ran inside (a code-mode `execute`, a delegated agent); `null` for a top-level call. */ parentId: string | null; /** * Calls nested under this one within the same block, in stream order. Always empty for a call * nothing names as its parent. */ children: TranscriptToolCall[]; /** Who has to decide, when the runner said so. See {@link TranscriptApproval}. */ approval: TranscriptApproval | null; /** * How to talk about this call without naming it — status, phrase, label, icon, approval prompt, * resolved result — from the server's presentation when `toolCatalog` was given, generic otherwise. */ description: ToolCallDescription; /** * Parked on a person. An `action` tool's input lands and its output never follows on its own — * the loop waits for an approval between the two — so a settled-looking card that never settles * IS the pending approval. */ isAwaitingApproval: boolean; /** `run({ remember: true })` approves this tool for the rest of the thread. */ approve: TranscriptSettleState & { run: (options?: ApproveOptions) => void; }; reject: TranscriptSettleState; /** A failed decision — the call is still parked, so the affordance stays live. */ error: string | null; /** * The server's machine-readable reason for {@link error}, when it gave one. `run_not_active` * means the turn that asked has ended: nothing is waiting for the decision, and pressing again * will be refused again — word it yourself and let the person send the message again. */ errorCode: string | null; } /** * A run of CONSECUTIVE tool parts, grouped so a UI can collapse "5 tools ran" into one affordance. * Any non-tool part between two tool parts ends the run — including the AI SDK's `step-start` * markers, so tools from different steps never merge into one group. */ export interface TranscriptToolBlock { kind: 'tools'; key: string; parts: AnyToolUIPart[]; /** The same calls, each with its human-decision state. Same order as `parts`. */ calls: TranscriptToolCall[]; /** * The same calls as a tree: every call whose parent is NOT in this block, each carrying its * nested calls in `children`. Equal to `calls` when nothing is nested. */ roots: TranscriptToolCall[]; /** * The run folded into activity groups ("Database query ×3"), over `roots`, keyed by each tool's * presentation label (else its name). For another grouping, call `groupToolActivity` yourself. */ activity: ToolActivityGroup[]; } /** * A component the server pushed into the message (the stream's `ui` frame, or a persisted * `data-ui` part). Rendered by looking `component` up in the host's own registry. */ export interface TranscriptUiBlock { kind: 'ui'; key: string; /** The component's identity within the message. */ id: string; component: string; props: Record<string, unknown>; /** Schema version of `props`; `null` when the server did not stamp one. */ version: number | null; fallbackText?: string; componentVersions?: Record<string, number>; /** The tool call that pushed it (`ctx.emitUi`); `null` for a component pushed outside a tool. */ toolCallId: string | null; /** * A preview drawn while the model writes the call's arguments (genui `streaming: 'partial'`): * unvalidated, replaced in place by the final push. Absent on a final component. */ partial?: true; } /** One choice a question offers, with its live selection state. */ export interface TranscriptQuestionOption { value: string; label: string; /** A single character the request suggested as a shortcut; `null` when it suggested none. */ hotkey: string | null; isSelected: boolean; /** Pre-picked by the agent — what an untouched question submits as. */ isDefault: boolean; /** Single choice: replaces the selection. Multiple: adds or removes this value. */ select: () => void; } /** One question of a set, numbered against the whole. */ export interface TranscriptQuestion { id: string; prompt: string; /** A line of help under the prompt; `null` when the request gave none. */ description: string | null; /** * How the answer is typed — `{ type, placeholder?, required?, min?, max?, pattern? }` — or `null` * for a plain pick from `options`. Render the control from `input.type`; `select` still picks * from `options`. */ input: ElicitationInput | null; multiple: boolean; /** 1-based. The request carries every question up front, so "Question 1 of N" is honest. */ position: number; options: TranscriptQuestionOption[]; selected: string[]; /** * The user has not touched this question, so a submission leaves it out and the server applies * the same defaults it showed. Distinct from "selected happens to equal the defaults": only the * first is recorded as `defaulted` rather than as a choice the user made. */ isPristine: boolean; /** The first selected value, or `''` — what a single text/number/date field shows. */ value: string; /** * Set a typed answer from whatever the control produced (a string, a number, a checkbox's * boolean, a `Date`, a list); coerced to the question's canonical strings with `coerceAnswer`. * `null`/`''` clears it. Does nothing once the set settled. */ setValue: (raw: RawAnswer) => void; /** * Why the current selection would be refused (the server's own rules — `validateAnswer`), or * `null`. A pristine required question with no default reads `requires an answer`; show it once * the user tried to submit, or right away — the model does not decide that for you. */ error: string | null; } /** How a question set settled, once it did. */ export interface TranscriptElicitationOutcome { answers: Record<string, string[]>; /** The user declined to answer and let the agent proceed on its own picks. */ skipped: boolean; /** Questions filled from their own defaults rather than by the human. */ defaulted: string[]; /** The questions against the chosen labels, as the model read them back. */ summary: string | null; /** Who answered (or skipped) — an approval's `decidedBy`. `null` when the run did not record it. */ answeredBy: string | null; /** The surface it came through (`'web'`, `'slack'`, …) — an approval's `decidedVia`. */ answeredVia: string | null; } /** * A question set the run put to the user, parked until someone settles it — the intake an agent * declares and the model's own `ask` produce the same block, because the loop streams the same * frame for both. */ export interface TranscriptElicitationBlock { kind: 'elicitation'; key: string; /** The parked tool call — what `answer`/`skip` route by. */ toolCallId: string; preamble: string | null; questions: TranscriptQuestion[]; questionCount: number; /** Still waiting on a human. */ isPending: boolean; /** Every question's current selection is acceptable (no question has an `error`). */ isValid: boolean; outcome: TranscriptElicitationOutcome | null; /** A failed submission — the run is still parked, so the form stays live. */ error: string | null; /** The server's machine-readable reason for {@link error} (`run_not_active`, …), when it gave one. */ errorCode: string | null; answer: TranscriptSettleState; skip: TranscriptSettleState; } /** One retrieved passage, as it rides a retrieval tool call's output. Mirrors core's `Passage`. */ export interface RetrievedPassage { id: string; text: string; score: number; /** Citation-facing origin — a document title, URL or row id. */ source?: string; metadata?: Record<string, unknown>; } /** The passages of one origin, folded together so a citation line reads once per source. */ export interface TranscriptSource { /** The `source` string the retriever stamped, falling back to the passage id when it stamped none. */ id: string; label: string; passageCount: number; /** Highest relevance among this source's passages; the scale is the retriever's. */ topScore: number; passages: RetrievedPassage[]; } /** * The provenance behind an answer: a run of CONSECUTIVE retrieval tool parts, aggregated by origin. * Detection is structural — a tool output shaped `{ passages: [{ id, text, ... }] }` — because the * tool's NAME is not fixed: inject-mode retrieval persists as `retrieve`, and `createRetrievalTool` * lets the host rename `search_knowledge` to anything. */ export interface TranscriptSourcesBlock { kind: 'sources'; key: string; /** What was searched, when the tool call recorded a `{ query }` input. */ query: string | null; sources: TranscriptSource[]; passageCount: number; } export type TranscriptBlock = TranscriptTextBlock | TranscriptFilesBlock | TranscriptReasoningBlock | TranscriptToolBlock | TranscriptSourcesBlock | TranscriptElicitationBlock | TranscriptUiBlock; /** Where a question set's selection state is held, and where its settlement is sent. */ export interface ElicitationBlockOptions { /** The values a question is showing, or `undefined` while the user has not touched it. */ picked: (toolCallId: string, questionId: string) => string[] | undefined; pick: (toolCallId: string, questionId: string, values: string[]) => void; canAnswer: boolean; canSkip: boolean; answer: (toolCallId: string) => void; skip: (toolCallId: string) => void; /** Which decision this call is sending, or `null` for none. */ submitting: (toolCallId: string) => SettleAction | null; errorOf: (toolCallId: string) => string | null; /** The `code` of the error `errorOf` reports, when the server sent one. */ errorCodeOf?: (toolCallId: string) => string | null; } /** Where an approval decision is sent, and what the last one did. */ export interface ApprovalBlockOptions { canApprove: boolean; canReject: boolean; approve: (toolCallId: string, options?: ApproveOptions, target?: ApprovalTarget) => void; reject: (toolCallId: string, target?: ApprovalTarget) => void; /** Which decision this call is sending, or `null` for none. */ submitting: (toolCallId: string) => SettleAction | null; errorOf: (toolCallId: string) => string | null; /** The `code` of the error `errorOf` reports, when the server sent one. */ errorCodeOf?: (toolCallId: string) => string | null; } export interface BuildBlocksOptions { /** Disclosure lookup for a reasoning run; `isStreaming` is the fallback when untouched. */ isReasoningOpen: (key: string, isStreaming: boolean) => boolean; toggleReasoning: (key: string, open?: boolean) => void; /** * Lift retrieval tool parts out of the tool run into a `sources` block. Off by default, so a * renderer wired to draw tool cards keeps receiving them as the tool calls they are. */ sources?: boolean; /** * Lift a question set out of the tool run into an `elicitation` block. Omitted → it stays a tool * card, which is all a host that has nowhere to send an answer could render anyway. */ elicitation?: ElicitationBlockOptions; /** Wire approve/reject onto the calls parked on a human. Omitted → they are reported, not actionable. */ approval?: ApprovalBlockOptions; /** Server-declared tool presentations (`useToolCatalog`), for each call's `description`. */ toolCatalog?: ToolCatalog; /** * Leave out a failed call the model retried at once with the same tool ({@link retriedCallIds}): * the person sees the view the retry drew, not the refusal before it. Default `true`; `false` * keeps every attempt (an operator's view). */ hideRetriedFailures?: boolean; } /** * Walk a message's parts into renderable blocks, buffering consecutive tool parts into one run and * consecutive files into one strip. A `data-ui` part becomes a `ui` block. A * `data-approval-requested` part is metadata about a call — it is folded into that call's * `approval` and takes no position of its own. Other `data-*` parts are dropped rather than guessed * at, but they still terminate a tool run — their position in the transcript is meaningful even * when their content is not modelled here. */ export declare function buildTranscriptBlocks(message: UIMessage, options: BuildBlocksOptions): TranscriptBlock[]; /** The message's prose, joined for the clipboard. Reasoning is excluded — it is not the answer. */ export declare function extractMessageText(parts: UIMessage['parts'] | undefined): string; export interface UsageSummary extends MessageUsageInfo { totalTokens: number; /** `$0`, `$0.0123`, `$0.012`, `$1.23` — precision follows magnitude so sub-cent turns stay legible. */ costLabel: string; tokensLabel: string; } export declare function describeUsage(usage: MessageUsageInfo): UsageSummary; export interface TimestampInfo { iso: string; date: Date; /** "just now" / "5 min. ago" / "Mar 3" — see {@link formatRelativeTime}. */ relative: string; absolute: string; } /** `null` for an unparseable stamp, so a bad `created_at` renders nothing instead of "Invalid Date". */ export declare function describeTimestamp(createdAt: string | null | undefined): TimestampInfo | null; /** * "just now" within ±10s (server `created_at` can land a few ms ahead of the client clock), a * relative phrase up to a week, then a calendar date. Uses Intl.RelativeTimeFormat — no date-fns. */ export declare function formatRelativeTime(date: Date): string; //# sourceMappingURL=model.d.ts.map