@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
750 lines • 30.1 kB
JavaScript
import { getToolName, isDataUIPart, isFileUIPart, isReasoningUIPart, isTextUIPart, isToolUIPart, } from 'ai';
import { readElicitationInput, validateElicitationAnswer, } from '../../../elicitation-input.js';
import { readUiActionText } from '../../../genui/actions.js';
import { coerceAnswer } from '../elicitation/answers.js';
import { describeToolCall, groupToolActivity, retriedCallIds, } from '../presentation/tool-activity.js';
import { readReasoningMs } from '../reasoning/timing.js';
/**
* 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 function buildTranscriptBlocks(message, options) {
const blocks = [];
let toolBuffer = [];
let retrievalBuffer = [];
let fileBuffer = [];
let toolCounter = 0;
let textCounter = 0;
let fileCounter = 0;
let reasoningCounter = 0;
let sourcesCounter = 0;
let elicitationCounter = 0;
const approvals = readApprovals(message.parts ?? []);
function flushTools() {
if (toolBuffer.length === 0) {
return;
}
const calls = toolBuffer.map((part) => buildToolCall(part, options.approval, approvalOf(approvals.get(part.toolCallId), part), options.toolCatalog));
const roots = nestToolCalls(calls);
blocks.push({
kind: 'tools',
key: `${message.id}-tools-${toolCounter++}`,
parts: toolBuffer,
calls,
roots,
activity: groupToolActivity(roots, {
...(options.toolCatalog !== undefined ? { catalog: options.toolCatalog } : {}),
}),
});
toolBuffer = [];
}
function flushSources() {
if (retrievalBuffer.length === 0) {
return;
}
blocks.push(buildSourcesBlock(`${message.id}-sources-${sourcesCounter++}`, retrievalBuffer));
retrievalBuffer = [];
}
function flushFiles() {
if (fileBuffer.length === 0) {
return;
}
blocks.push({
kind: 'files',
key: `${message.id}-files-${fileCounter++}`,
files: fileBuffer.map((part) => ({
url: part.url,
mediaType: part.mediaType,
filename: part.filename ?? null,
isImage: part.mediaType.startsWith('image/'),
})),
});
fileBuffer = [];
}
function flushAll() {
flushTools();
flushSources();
flushFiles();
}
const retried = options.hideRetriedFailures === false
? new Set()
: retriedCallIds((message.parts ?? []).filter(isToolUIPart));
const settledCalls = new Set();
for (const part of message.parts ?? []) {
if (isToolUIPart(part) &&
(part.state === 'output-available' ||
part.state === 'output-error' ||
part.state === 'output-denied')) {
settledCalls.add(part.toolCallId);
}
}
for (const part of message.parts ?? []) {
if (isToolUIPart(part)) {
if (retried.has(part.toolCallId))
continue;
const elicitation = options.elicitation;
if (elicitation !== undefined) {
const request = readElicitationRequest(part);
if (request !== null) {
flushAll();
blocks.push(buildElicitationBlock(`${message.id}-elicitation-${elicitationCounter++}`, part, request, elicitation));
continue;
}
}
if (options.sources === true && readPassages(part) !== null) {
flushTools();
retrievalBuffer.push(part);
continue;
}
flushSources();
flushFiles();
toolBuffer.push(part);
continue;
}
if (isFileUIPart(part)) {
flushTools();
flushSources();
fileBuffer.push(part);
continue;
}
if (part.type === APPROVAL_PART || part.type === SETTLED_PART) {
continue;
}
flushAll();
if (isDataUIPart(part)) {
const ui = part.type === UI_PART ? readUiComponent(part) : null;
// A preview stands only while its call is being written: withdrawn (no props), or left by a
// call that settled without replacing it, it is nothing to draw.
if (ui?.partial === true &&
(Object.keys(ui.props).length === 0 ||
(ui.toolCallId !== null && settledCalls.has(ui.toolCallId)))) {
continue;
}
if (ui !== null) {
blocks.push({ kind: 'ui', key: `${message.id}-ui-${ui.id}`, ...ui });
}
continue;
}
if (isTextUIPart(part)) {
const uiAction = message.role === 'user' ? readUiActionText(part.text) : null;
blocks.push({
kind: 'text',
key: `${message.id}-text-${textCounter++}`,
text: part.text,
isStreaming: part.state === 'streaming',
...(uiAction !== null ? { uiAction } : {}),
});
continue;
}
if (isReasoningUIPart(part)) {
const key = `${message.id}-reasoning-${reasoningCounter++}`;
const isStreaming = part.state === 'streaming';
blocks.push({
kind: 'reasoning',
key,
text: part.text,
isStreaming,
durationMs: readReasoningMs(part),
isOpen: options.isReasoningOpen(key, isStreaming),
toggle: (open) => options.toggleReasoning(key, open),
});
}
}
flushAll();
return blocks;
}
/**
* Fold a run of retrieval parts into one provenance block. Sources keep the order the retriever
* returned them in — that order IS the ranking — and the query is taken from the first call that
* recorded one.
*/
function buildSourcesBlock(key, parts) {
const byId = new Map();
let query = null;
let passageCount = 0;
for (const part of parts) {
query ??= readQuery(part);
for (const passage of readPassages(part) ?? []) {
passageCount++;
const id = passage.source ?? passage.id;
const existing = byId.get(id);
if (existing) {
existing.passageCount++;
existing.topScore = Math.max(existing.topScore, passage.score);
existing.passages.push(passage);
continue;
}
byId.set(id, {
id,
label: passage.source ?? passage.id,
passageCount: 1,
topScore: passage.score,
passages: [passage],
});
}
}
return { kind: 'sources', key, query, sources: [...byId.values()], passageCount };
}
/** A call whose output has landed one way or another is nobody's decision any more. */
function isSettled(state) {
return state === 'output-available' || state === 'output-error' || state === 'output-denied';
}
function toolKind(part) {
const metadata = part.toolMetadata;
return isRecord(metadata) && typeof metadata.toolKind === 'string'
? metadata.toolKind
: undefined;
}
/**
* An `action` tool's input lands and then nothing follows: the loop waits on a person between the
* call and its execution. So a call classified `action` and stuck at `input-available` IS the
* pending approval — there is no separate frame that says so.
*/
function isAwaitingApproval(part) {
if (part.state === 'approval-requested') {
return true;
}
if (part.state !== 'input-available') {
return false;
}
// A question set parks the same way and is persisted as an action, but it is settled by
// answering it, not by approving it.
return toolKind(part) === 'action' && readElicitationRequest(part) === null;
}
function toolParentId(part) {
const metadata = part.toolMetadata;
return isRecord(metadata) && typeof metadata.parentId === 'string' ? metadata.parentId : null;
}
/**
* Arrange a block's calls into a tree by `parentId`. A call whose parent is not in the block (or
* that names itself, or would close a cycle) stays a root, so nothing a runner sends is ever lost
* from the tree — at worst it is shown flat.
*/
function nestToolCalls(calls) {
const byId = new Map(calls.map((call) => [call.toolCallId, call]));
const roots = [];
for (const call of calls) {
const parent = call.parentId !== null ? byId.get(call.parentId) : undefined;
if (parent === undefined || isAncestor(call, parent, byId)) {
roots.push(call);
continue;
}
parent.children.push(call);
}
return roots;
}
/** True when `candidate` is `call` itself or sits under it — attaching would close a cycle. */
function isAncestor(call, candidate, byId) {
const seen = new Set();
let current = candidate;
while (current !== undefined && !seen.has(current.toolCallId)) {
if (current.toolCallId === call.toolCallId) {
return true;
}
seen.add(current.toolCallId);
current = current.parentId !== null ? byId.get(current.parentId) : undefined;
}
return false;
}
function buildToolCall(part, options, approval, catalog) {
const toolCallId = part.toolCallId;
const awaiting = approval?.target?.kind === 'proposal'
? approval.status === 'pending'
: isAwaitingApproval(part);
// Per decision, not per call: the two are never in flight together, and a surface that read one
// flag for both would report the refusal it is carrying out as an approval in progress.
const sending = awaiting ? (options?.submitting(toolCallId) ?? null) : null;
return {
part,
toolCallId,
name: getToolName(part),
toolKind: toolKind(part) ?? null,
parentId: toolParentId(part),
children: [],
approval,
description: proposalDescription(describeToolCall(part, catalog, approval?.confirmation === undefined ? {} : { confirmation: approval.confirmation }), approval),
isAwaitingApproval: awaiting,
approve: {
available: awaiting && options?.canApprove === true,
isSubmitting: sending === 'approve',
// Read by field, not passed through: `onClick={call.approve.run}` hands this a click event,
// and a host's handler must not receive that as its options.
run: (approveOptions) => approval?.target === undefined
? approveOptions?.remember === true
? options?.approve(toolCallId, { remember: true })
: options?.approve(toolCallId)
: options?.approve(toolCallId, approveOptions?.remember === true ? { remember: true } : undefined, approval.target),
},
reject: {
available: awaiting && options?.canReject === true,
isSubmitting: sending === 'reject',
run: () => approval?.target === undefined
? options?.reject(toolCallId)
: options?.reject(toolCallId, approval.target),
},
error: options?.errorOf(toolCallId) ?? null,
errorCode: options?.errorCodeOf?.(toolCallId) ?? null,
};
}
/**
* The question set a tool call carries, or `null` for any other tool. Read from the call's INPUT
* and by shape rather than by tool name: the same set is authored by an agent's intake and by the
* model's own `ask`, it replays from the store under whatever name the row holds, and a client
* that learned to recognise one name would render only half of them.
*/
function readElicitationRequest(part) {
const input = part.input;
if (!isRecord(input) || !Array.isArray(input.questions) || input.questions.length === 0) {
return null;
}
const questions = [];
for (const candidate of input.questions) {
if (!isRecord(candidate) ||
typeof candidate.id !== 'string' ||
typeof candidate.prompt !== 'string') {
return null;
}
// A typed question may carry no options; anything else is a pick and must offer some.
const input = readElicitationInput(candidate.input) ?? null;
const rawOptions = Array.isArray(candidate.options) ? candidate.options : [];
if (rawOptions.length === 0 && (input === null || input.type === 'select')) {
return null;
}
const options = [];
for (const option of rawOptions) {
if (!isRecord(option) ||
typeof option.value !== 'string' ||
typeof option.label !== 'string') {
return null;
}
options.push({
value: option.value,
label: option.label,
hotkey: typeof option.hotkey === 'string' ? option.hotkey : null,
});
}
questions.push({
id: candidate.id,
prompt: candidate.prompt,
description: typeof candidate.description === 'string' ? candidate.description : null,
input,
multiple: candidate.multiple === true,
allowFreeText: candidate.allowFreeText === true,
defaults: Array.isArray(candidate.defaults)
? candidate.defaults.filter((value) => typeof value === 'string')
: [],
options,
});
}
return {
preamble: typeof input.preamble === 'string' ? input.preamble : null,
questions,
};
}
/** What the run recorded for a settled question set, or `null` when it settled as something else. */
function readElicitationOutcome(part) {
const output = part.output;
if (!isRecord(output) || !isRecord(output.answers) || typeof output.skipped !== 'boolean') {
return null;
}
const answers = {};
for (const [id, values] of Object.entries(output.answers)) {
answers[id] = Array.isArray(values)
? values.filter((value) => typeof value === 'string')
: [];
}
return {
answers,
skipped: output.skipped,
defaulted: Array.isArray(output.defaulted)
? output.defaulted.filter((id) => typeof id === 'string')
: [],
summary: typeof output.summary === 'string' ? output.summary : null,
answeredBy: typeof output.answeredBy === 'string' ? output.answeredBy : null,
answeredVia: typeof output.answeredVia === 'string' ? output.answeredVia : null,
};
}
function buildElicitationBlock(key, part, request, options) {
const toolCallId = part.toolCallId;
const isPending = !isSettled(part.state);
const outcome = isPending ? null : readElicitationOutcome(part);
const questions = request.questions.map((question, index) => {
const picked = isPending ? options.picked(toolCallId, question.id) : undefined;
const selected = picked ?? outcome?.answers[question.id] ?? question.defaults;
const pick = (values) => options.pick(toolCallId, question.id, values);
// The same rules the answer route applies, over the same question shape core reads.
const error = isPending
? validateElicitationAnswer({
id: question.id,
prompt: question.prompt,
options: question.options.map(({ value, label }) => ({ value, label })),
...(question.input !== null ? { input: question.input } : {}),
...(question.multiple ? { multiple: true } : {}),
...(question.allowFreeText ? { allowFreeText: true } : {}),
}, selected)
: null;
return {
id: question.id,
prompt: question.prompt,
description: question.description,
input: question.input,
multiple: question.multiple,
position: index + 1,
selected,
value: selected[0] ?? '',
error,
setValue: (raw) => {
if (isPending) {
pick(coerceAnswer(question, raw));
}
},
isPristine: isPending
? picked === undefined
: (outcome?.defaulted.includes(question.id) ?? true),
options: question.options.map((option) => ({
value: option.value,
label: option.label,
hotkey: option.hotkey,
isSelected: selected.includes(option.value),
isDefault: question.defaults.includes(option.value),
select: () => {
if (!isPending) {
return;
}
if (!question.multiple) {
pick([option.value]);
return;
}
pick(selected.includes(option.value)
? selected.filter((value) => value !== option.value)
: [...selected, option.value]);
},
})),
};
});
const sending = isPending ? options.submitting(toolCallId) : null;
return {
kind: 'elicitation',
key,
toolCallId,
preamble: request.preamble,
questions,
questionCount: questions.length,
isPending,
isValid: questions.every((question) => question.error === null),
outcome,
error: options.errorOf(toolCallId),
errorCode: options.errorCodeOf?.(toolCallId) ?? null,
answer: {
available: isPending && options.canAnswer,
isSubmitting: sending === 'answer',
run: () => options.answer(toolCallId),
},
skip: {
available: isPending && options.canSkip,
isSubmitting: sending === 'skip',
run: () => options.skip(toolCallId),
},
};
}
const UI_PART = 'data-ui';
const APPROVAL_PART = 'data-approval-requested';
const SETTLED_PART = 'data-approval-settled';
const SETTLED_STATUSES = ['approved', 'rejected', 'expired', 'superseded'];
/** The approval metadata on a message, by call id. The latest frame of each kind for a call wins. */
function readApprovals(parts) {
const out = new Map();
const entry = (id) => {
let found = out.get(id);
if (found === undefined) {
found = {
approver: null,
expiresAt: null,
reason: null,
status: null,
remember: false,
decidedBy: null,
decidedVia: null,
decisionReason: null,
};
out.set(id, found);
}
return found;
};
for (const part of parts) {
if (part.type !== APPROVAL_PART &&
part.type !== SETTLED_PART &&
part.type !== 'data-action-proposal') {
continue;
}
const data = part.data;
if (!isRecord(data) || typeof data.id !== 'string') {
continue;
}
const approval = entry(data.id);
if (part.type === APPROVAL_PART || part.type === 'data-action-proposal') {
if (isRecord(data.target) &&
data.target.kind === 'proposal' &&
typeof data.target.proposalId === 'string') {
approval.target = {
kind: 'proposal',
proposalId: data.target.proposalId,
...(typeof data.target.threadId === 'string' ? { threadId: data.target.threadId } : {}),
};
}
if (part.type === 'data-action-proposal') {
if (typeof data.status === 'string' &&
['pending', 'approved', 'rejected', 'expired', 'superseded'].includes(data.status))
approval.status =
data.status === 'pending'
? null
: data.status;
if (data.executionStatus === null ||
data.executionStatus === 'queued' ||
data.executionStatus === 'executing' ||
data.executionStatus === 'succeeded' ||
data.executionStatus === 'failed')
approval.executionStatus = data.executionStatus;
approval.remember = data.remember === true;
approval.decidedBy = typeof data.decidedBy === 'string' ? data.decidedBy : null;
approval.decidedVia = typeof data.decidedVia === 'string' ? data.decidedVia : null;
}
if (typeof data.approver !== 'string') {
continue;
}
approval.approver = data.approver;
approval.expiresAt = typeof data.expiresAt === 'string' ? data.expiresAt : null;
if (isRecord(data.confirmation) &&
typeof data.confirmation.title === 'string' &&
typeof data.confirmation.verb === 'string' &&
(data.confirmation.detail === undefined || typeof data.confirmation.detail === 'string')) {
approval.confirmation = {
title: data.confirmation.title,
verb: data.confirmation.verb,
...(typeof data.confirmation.detail === 'string'
? { detail: data.confirmation.detail }
: {}),
};
}
approval.reason = typeof data.reason === 'string' ? data.reason : null;
continue;
}
if (typeof data.status !== 'string' || !SETTLED_STATUSES.includes(data.status)) {
continue;
}
approval.status = data.status;
if (approval.approver === null && typeof data.approver === 'string') {
approval.approver = data.approver;
}
approval.remember = data.remember === true;
approval.decidedBy = typeof data.decidedBy === 'string' ? data.decidedBy : null;
approval.decidedVia = typeof data.decidedVia === 'string' ? data.decidedVia : null;
approval.decisionReason = typeof data.reason === 'string' ? data.reason : null;
}
return out;
}
/**
* One call's approval: its parts, with the status completed from the call's own state when no
* settlement was streamed. `null` when nothing on the message says who decides.
*/
function approvalOf(parts, call) {
if (parts === undefined || parts.approver === null) {
return null;
}
return {
approver: parts.approver,
...(parts.target === undefined ? {} : { target: parts.target }),
...(parts.executionStatus === undefined ? {} : { executionStatus: parts.executionStatus }),
...(parts.confirmation !== undefined ? { confirmation: parts.confirmation } : {}),
expiresAt: parts.expiresAt,
reason: parts.reason,
status: parts.status ?? (parts.target?.kind === 'proposal' ? 'pending' : statusFromPart(call)),
remember: parts.remember,
decidedBy: parts.decidedBy,
decidedVia: parts.decidedVia,
decisionReason: parts.decisionReason,
};
}
function statusFromPart(call) {
if (call.state === 'output-denied') {
return 'rejected';
}
if (call.state === 'output-available' || call.state === 'output-error') {
return 'approved';
}
return 'pending';
}
/** A pushed component, or `null` when the part does not carry one this model can address. */
function readUiComponent(part) {
const data = part.data;
if (!isRecord(data) || typeof data.component !== 'string') {
return null;
}
const id = typeof data.id === 'string' ? data.id : part.id;
if (id === undefined) {
return null;
}
return {
id,
component: data.component,
props: isRecord(data.props) ? data.props : {},
version: typeof data.version === 'number' ? data.version : null,
...(isRecord(data.componentVersions) &&
Object.values(data.componentVersions).every((version) => typeof version === 'number' && Number.isSafeInteger(version) && version > 0)
? { componentVersions: { ...data.componentVersions } }
: {}),
...(typeof data.fallbackText === 'string' ? { fallbackText: data.fallbackText } : {}),
toolCallId: typeof data.toolCallId === 'string' ? data.toolCallId : null,
...(data.partial === true ? { partial: true } : {}),
};
}
function isRecord(value) {
return typeof value === 'object' && value !== null;
}
function readQuery(part) {
const input = part.input;
if (!isRecord(input) || typeof input.query !== 'string') {
return null;
}
return input.query;
}
/**
* The passages on a settled retrieval tool call, or `null` for any other tool. A call still in
* flight, or one whose output is not a non-empty list of `{ id, text }` passages, is not provenance
* yet and stays an ordinary tool part.
*/
function readPassages(part) {
const output = part.output;
if (!isRecord(output) || !Array.isArray(output.passages) || output.passages.length === 0) {
return null;
}
const passages = [];
for (const candidate of output.passages) {
if (!isRecord(candidate) ||
typeof candidate.id !== 'string' ||
typeof candidate.text !== 'string') {
return null;
}
passages.push({
id: candidate.id,
text: candidate.text,
score: typeof candidate.score === 'number' ? candidate.score : 0,
...(typeof candidate.source === 'string' ? { source: candidate.source } : {}),
...(isRecord(candidate.metadata) ? { metadata: candidate.metadata } : {}),
});
}
return passages;
}
/** The message's prose, joined for the clipboard. Reasoning is excluded — it is not the answer. */
export function extractMessageText(parts) {
const out = [];
for (const part of parts ?? []) {
if (isTextUIPart(part)) {
out.push(part.text);
}
}
return out.join('\n\n').trim();
}
export function describeUsage(usage) {
const totalTokens = usage.inputTokens + usage.outputTokens;
return {
...usage,
totalTokens,
costLabel: formatCostUsd(usage.costUsd),
tokensLabel: formatTokensShort(totalTokens),
};
}
/** `null` for an unparseable stamp, so a bad `created_at` renders nothing instead of "Invalid Date". */
export function describeTimestamp(createdAt) {
if (createdAt == null) {
return null;
}
const date = new Date(createdAt);
if (Number.isNaN(date.getTime())) {
return null;
}
return {
iso: createdAt,
date,
relative: formatRelativeTime(date),
absolute: date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }),
};
}
const RELATIVE_TIME = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto', style: 'narrow' });
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
/**
* "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 function formatRelativeTime(date) {
const diffMs = Date.now() - date.getTime();
if (Math.abs(diffMs) < 10_000) {
return 'just now';
}
if (diffMs > ONE_WEEK_MS) {
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}
const minutes = Math.round(-diffMs / 60_000);
if (Math.abs(minutes) < 60) {
return RELATIVE_TIME.format(minutes, 'minute');
}
const hours = Math.round(-diffMs / 3_600_000);
if (Math.abs(hours) < 24) {
return RELATIVE_TIME.format(hours, 'hour');
}
return RELATIVE_TIME.format(Math.round(-diffMs / 86_400_000), 'day');
}
function formatTokensShort(n) {
if (n < 1_000) {
return `${n} tokens`;
}
if (n < 10_000) {
return `${(n / 1_000).toFixed(1)}k tokens`;
}
return `${Math.round(n / 1_000)}k tokens`;
}
function formatCostUsd(cost) {
if (cost === null) {
return '—';
}
if (cost === 0) {
return '$0';
}
if (cost < 0.01) {
return `$${cost.toFixed(4)}`;
}
if (cost < 1) {
return `$${cost.toFixed(3)}`;
}
return `$${cost.toFixed(2)}`;
}
function proposalDescription(description, approval) {
if (approval?.target?.kind !== 'proposal')
return description;
const status = approval.status === 'pending'
? 'awaiting-approval'
: approval.status === 'approved'
? (approval.executionStatus ?? 'queued')
: 'denied';
const phrase = status === 'awaiting-approval'
? 'Awaiting approval'
: status === 'queued'
? 'Queued'
: status === 'executing'
? 'Executing'
: status === 'succeeded'
? 'Succeeded'
: status === 'failed'
? 'Failed'
: approval.status === 'expired'
? 'Expired'
: approval.status === 'superseded'
? 'Replaced'
: 'Rejected';
return { ...description, status, phrase, result: null };
}
//# sourceMappingURL=model.js.map