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