@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
311 lines • 16.1 kB
JavaScript
import { getTelescopeGovernanceQueries } from './governance-registry.js';
/**
* Governance data providers for the "Agent" Telescope tab: spend/usage, run reliability, tool
* activity, and the pending-approvals inbox. Unlike `data-providers.ts` (which reads the ephemeral,
* capped `lib:agent` diagnostic entries Telescope's generic bridge records), these read the
* authoritative, restart-surviving read-model — `AgentGovernanceQueries` — via
* `governance-registry.ts` (see that file for why a module-internal registry stands in for Nest's
* DI-resolved `AGENT_GOVERNANCE_QUERIES`).
*
* ## Ported from the Nest reference with real gaps, not just renamed fields
*
* Adonis's `AgentGovernanceQueries` (`../spi/governance-queries.js`) is NOT the same interface as
* nestjs-agent-core's — it is a separately-designed SPI that happens to cover the same ground. Some
* of Nest's governance panels have no Adonis equivalent to bind to, and are deliberately NOT ported
* here rather than faked against a shape that doesn't exist:
*
* - **Top threads by cost** (`spendByThread`) — no such query on `AgentGovernanceQueries`.
* - **Run retries** — `RunReliability` has no retry counter at all (Nest's `RunMetrics.retries`
* counts LLM-step retries; Adonis's read-model doesn't aggregate that dimension).
* - **Run duration percentiles** — `agent.runs.duration` below stays the mean. `RunReliability`
* now carries an optional `latency` (TTFT and turn duration p50/p95) and `failuresByCode`, but an
* adapter may omit both; the tab charts them from entries instead (`agent.latency`,
* `agent.failuresByCode` in `data-providers.ts`), which every host has.
* - **Paged tool-calls / threads / runs tables** (`{ rows, total, page, limit }`, `Panel.paged`) —
* `AgentGovernanceQueries` has no page+total query for tool calls or threads at all
* (`recentToolCalls`/`recentThreads` are capped lists with no total), and `listRuns` paginates by
* an opaque CURSOR, not a page number, so it cannot answer "page 3 of 7" either. `agent.runs.recent`
* below uses `listRuns` for its FIRST page only, same as Nest's `recentRuns(limit)` — the capped,
* non-paged shape both ecosystems already had before A1's paged-table convention existed.
* - **Exact pending-approvals count** — `pendingApprovals` returns an array with no `total`; unlike
* Nest's `approvalsPage({ page: 1, pageSize: 1 }).total`, there is no cheap way to ask "how many,
* without listing them". `agentPendingApprovalsCountProvider` below is therefore a real
* approximation — see its own doc comment.
*
* Every provider still degrades to an empty-but-valid shape (0 / `[]`) when the read-model isn't
* bound, exactly like the Nest reference — governance is optional configuration
* (`config.governanceQueries`), and telescope must render something sane when it's off.
*/
/** Default trailing window (in days, inclusive) when a panel query omits an explicit range. */
const DEFAULT_TREND_WINDOW_DAYS = 30;
const ISO_DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
function isIsoDay(value) {
return typeof value === 'string' && ISO_DAY_PATTERN.test(value);
}
function todayUtcDay() {
return new Date().toISOString().slice(0, 10);
}
/** Shift a `YYYY-MM-DD` UTC day by `deltaDays` (negative goes back in time). */
export function shiftUtcDay(day, deltaDays) {
const shifted = new Date(`${day}T00:00:00.000Z`);
shifted.setUTCDate(shifted.getUTCDate() + deltaDays);
return shifted.toISOString().slice(0, 10);
}
/**
* Derive the query range: honour explicit `fromDay`/`toDay` (validated ISO days), otherwise default
* to the trailing {@link DEFAULT_TREND_WINDOW_DAYS}-day window ending today (UTC).
*/
export function resolveRange(query) {
const toDay = query && isIsoDay(query.toDay) ? query.toDay : todayUtcDay();
const fromDay = query && isIsoDay(query.fromDay)
? query.fromDay
: shiftUtcDay(toDay, -(DEFAULT_TREND_WINDOW_DAYS - 1));
return { fromDay, toDay };
}
function roundCents(value) {
return Math.round(value * 100) / 100;
}
/** Read a positive number out of a raw query value — panel queries reach a provider as strings. */
function readPositiveNumber(raw) {
if (typeof raw === 'number')
return Number.isFinite(raw) && raw > 0 ? raw : undefined;
if (typeof raw !== 'string' || raw.trim() === '')
return undefined;
const parsed = Number(raw);
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}
/** Default row count for a "recent …" feed when a panel's query omits `limit`. */
const DEFAULT_RECENT_LIMIT = 50;
/** Hard ceiling on `limit`, regardless of what a panel's query requests. */
const MAX_RECENT_LIMIT = 500;
/** Clamp a panel-supplied `limit` into `[1, MAX_RECENT_LIMIT]`, defaulting when absent/invalid. */
function resolveLimit(query, fallback) {
const value = readPositiveNumber(query?.limit) ?? fallback;
return Math.min(MAX_RECENT_LIMIT, Math.max(1, Math.trunc(value)));
}
/** Placeholder shown for a table cell whose source value is `null`/unmeasured. */
const NO_VALUE = '—';
/** Cap this many characters before an error message rides into a table cell (matches the DB column's practical display width). */
const ERROR_MESSAGE_CAP = 500;
/** Cap a run's error message to {@link ERROR_MESSAGE_CAP} characters. `resolve()` output bypasses
* Telescope's entry redaction pipeline (that only runs on Watcher-recorded entries) — this cap is a
* stopgap so an unbounded stack trace never rides straight into a table cell unbounded. */
export function capErrorMessage(message) {
if (message === null)
return NO_VALUE;
return message.length > ERROR_MESSAGE_CAP ? `${message.slice(0, ERROR_MESSAGE_CAP)}…` : message;
}
/** Build a governance `DataProvider` from a fetch + a format step. Degrades to `format([])` when the
* read-model isn't bound — the empty-but-valid shape every formatter already yields for `[]`. */
function governanceProvider(name, arg, fetch, format) {
return {
name,
async resolve(query, _ctx) {
const queries = getTelescopeGovernanceQueries();
const rows = queries ? await fetch(queries, arg(query)) : [];
return format(rows);
},
};
}
/** stat → authoritative total spend (USD) over the range. */
export function agentSpendTotalProvider() {
return governanceProvider('agent.spend.totalCost', resolveRange, (queries, range) => queries.spendByModel(range), (rows) => ({
value: roundCents(rows.reduce((sum, row) => sum + row.costUsd, 0)),
}));
}
/** stat → authoritative total tokens (input + output) over the range. */
export function agentTokensTotalProvider() {
return governanceProvider('agent.spend.totalTokens', resolveRange, (queries, range) => queries.spendByModel(range), (rows) => ({
value: rows.reduce((sum, row) => sum + row.inputTokens + row.outputTokens, 0),
}));
}
/** breakdown → spend share per model (models with zero cost dropped from the donut). */
export function agentSpendByModelProvider() {
return governanceProvider('agent.spend.byModel', resolveRange, (queries, range) => queries.spendByModel(range), (rows) => ({
segments: rows
.filter((row) => row.costUsd > 0)
.map((row) => ({ label: row.modelId, value: roundCents(row.costUsd) })),
}));
}
/** table → per-model requests / in+out tokens / cost. */
export function agentModelSpendTableProvider() {
return governanceProvider('agent.spend.byModelTable', resolveRange, (queries, range) => queries.spendByModel(range), (rows) => ({
rows: rows.map((row) => ({ ...row, costUsd: roundCents(row.costUsd) })),
}));
}
/** timeseries → daily spend + tokens trend. */
export function agentUsageTrendProvider() {
return governanceProvider('agent.usage.trend', resolveRange, (queries, range) => queries.usageTrend(range), (points) => ({
rows: points.map((point) => ({
label: point.day,
costUsd: roundCents(point.costUsd),
totalTokens: point.totalTokens,
})),
}));
}
/** table → spend per acting ref (user/tenant). */
export function agentActorSpendTableProvider() {
return governanceProvider('agent.spend.byActor', resolveRange, (queries, range) => queries.spendByActor(range), (rows) => ({
rows: rows.map((row) => ({ ...row, costUsd: roundCents(row.costUsd) })),
}));
}
/** breakdown → spend share per actor (actors with zero cost dropped). */
export function agentSpendByActorProvider() {
return governanceProvider('agent.spend.byActorShare', resolveRange, (queries, range) => queries.spendByActor(range), (rows) => ({
segments: rows
.filter((row) => row.costUsd > 0)
.map((row) => ({ label: row.actorRef, value: roundCents(row.costUsd) })),
}));
}
/** Build a `DataProvider` off ONE range-scoped `runReliability` call + a format step — every
* reliability stat below reuses the same underlying query. Degrades to zeros when unbound. */
function reliabilityProvider(name, format) {
return {
name,
async resolve(query, _ctx) {
const queries = getTelescopeGovernanceQueries();
const metrics = queries
? await queries.runReliability(toToolStatsRange(query))
: EMPTY_RELIABILITY;
return format(metrics);
},
};
}
const EMPTY_RELIABILITY = {
runs: 0,
successRate: 0,
failed: 0,
avgDurationMs: null,
};
/** `runReliability`'s range is `{ from?, to? }` (both optional), not `resolveRange`'s `{ fromDay, toDay }`
* (both required) — a panel that omits `fromDay`/`toDay` here means "no lower/upper bound", not "default
* trailing 30 days", matching what an unscoped reliability query already means on the SPI. */
function toToolStatsRange(query) {
const from = query && isIsoDay(query.fromDay) ? query.fromDay : undefined;
const to = query && isIsoDay(query.toDay) ? query.toDay : undefined;
return { ...(from !== undefined ? { from } : {}), ...(to !== undefined ? { to } : {}) };
}
/** stat → total runs over the range. */
export function agentRunsTotalProvider() {
return reliabilityProvider('agent.runs.total', (metrics) => ({ value: metrics.runs }));
}
/** stat → completed/total success rate over the range. */
export function agentRunsSuccessRateProvider() {
return reliabilityProvider('agent.runs.successRate', (metrics) => ({
value: metrics.successRate,
}));
}
/** stat → failed run count over the range. */
export function agentRunsFailedProvider() {
return reliabilityProvider('agent.runs.failed', (metrics) => ({ value: metrics.failed }));
}
/**
* stat → MEAN settled-run duration over the range. Not a percentile: `RunReliability` exposes only
* `avgDurationMs` (see this file's header) — there is no p50/p95 to select between, unlike the Nest
* reference's `query.metric` switch. `null` (no settled runs) resolves to 0.
*/
export function agentRunsDurationProvider() {
return reliabilityProvider('agent.runs.duration', (metrics) => ({
value: metrics.avgDurationMs ?? 0,
}));
}
/** table → run/failure rollup per agent (empty when the adapter predates `byAgent`). */
export function agentRunsByAgentTableProvider() {
return reliabilityProvider('agent.runs.byAgent', (metrics) => ({
rows: (metrics.byAgent ?? []).map((row) => ({
agentName: row.agentName ?? NO_VALUE,
runs: row.runs,
failed: row.failed,
})),
}));
}
/** timeseries → daily runs + failures trend (empty when the adapter predates `trend`). */
export function agentRunsTrendProvider() {
return reliabilityProvider('agent.runs.trend', (metrics) => ({
rows: (metrics.trend ?? []).map((point) => ({ label: point.day, runs: point.runs, failed: point.failed })),
}));
}
/** table → most recent runs (newest first), via `listRuns`' first page — see this file's header for
* why this isn't the paged-table protocol. errorMessage capped per {@link capErrorMessage}. */
export function agentRecentRunsTableProvider() {
return {
name: 'agent.runs.recent',
async resolve(query, _ctx) {
const queries = getTelescopeGovernanceQueries();
if (!queries)
return { rows: [] };
const limit = resolveLimit(query, DEFAULT_RECENT_LIMIT);
const { items } = await queries.listRuns({ first: limit });
return {
rows: items.map((row) => ({
startedAt: row.startedAt,
runId: row.runId,
threadId: row.threadId,
actorRef: row.actorRef,
agentName: row.agentName ?? NO_VALUE,
status: row.status,
durationMs: row.durationMs ?? NO_VALUE,
stepCount: row.stepCount,
costUsd: row.costUsd ?? NO_VALUE,
error: capErrorMessage(row.error),
})),
};
},
};
}
/** table → most recent tool calls (newest first), from the durable read-model. */
export function agentRecentToolCallsTableProvider() {
return governanceProvider('agent.tools.recent', (query) => resolveLimit(query, DEFAULT_RECENT_LIMIT), (queries, limit) => queries.recentToolCalls(limit), (rows) => ({ rows }));
}
/** table → most recently active threads, with rolled-up message/token counts. */
export function agentRecentThreadsTableProvider() {
return governanceProvider('agent.threads.recent', (query) => resolveLimit(query, DEFAULT_RECENT_LIMIT), (queries, limit) => queries.recentThreads(limit), (rows) => ({ rows }));
}
/**
* stat → an APPROXIMATE count of tool calls sitting `pending_approval`, capped at
* {@link MAX_RECENT_LIMIT}. `AgentGovernanceQueries.pendingApprovals` has no `total` — unlike the
* Nest reference's exact `approvalsPage({ page: 1, pageSize: 1 }).total`, there is no cheap "how
* many, without listing them" query on this SPI (see this file's header). A backlog past the cap
* undercounts here exactly the way Nest's OLD `pendingApprovals(500).length` did before it got an
* exact query — that regression is accepted, not fixed, until `AgentGovernanceQueries` grows one.
*/
export function agentPendingApprovalsCountProvider() {
return {
name: 'agent.approvals.pending',
async resolve(_query, _ctx) {
const queries = getTelescopeGovernanceQueries();
if (!queries)
return { value: 0 };
const rows = await queries.pendingApprovals({ limit: MAX_RECENT_LIMIT });
return { value: rows.length };
},
};
}
/** table → the pending-approvals inbox, oldest request first. */
export function agentPendingApprovalsTableProvider() {
return governanceProvider('agent.approvals.recent', (query) => resolveLimit(query, DEFAULT_RECENT_LIMIT), (queries, limit) => queries.pendingApprovals({ limit }), (rows) => ({
rows: rows.map((row) => ({
toolCallId: row.toolCallId,
toolName: row.toolName,
threadId: row.threadId,
actorRef: row.actorRef,
requestedAt: row.requestedAt,
runId: row.runId,
})),
}));
}
/** table → per-tool call/failure/rejection/latency rollup over the range. Only ONE latency column
* (`avgDurationMs`) — `PerToolStatRow` has no p50/p95 split, unlike the Nest reference's table. */
export function agentToolStatsTableProvider() {
return governanceProvider('agent.tools.stats', toToolStatsRange, (queries, range) => queries.perToolStats(range), (rows) => ({
rows: rows.map((row) => ({
toolName: row.toolName,
toolType: row.toolType,
calls: row.calls,
failed: row.failed,
rejected: row.rejected,
avgDurationMs: row.avgDurationMs ?? NO_VALUE,
})),
}));
}
//# sourceMappingURL=agent-governance-providers.js.map