UNPKG

@adonis-agora/agent

Version:

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

442 lines • 19.8 kB
import { percentile } from '../run-metrics.js'; /** * The agent "Agent" dashboard data providers. Every provider here is ENTRY-BACKED: it aggregates the * `agora:agent:*` lifecycle events that `@adonis-agora/agent` emits through `@adonis-agora/diagnostics` * (via `src/diagnostics.ts`'s `publishAgent`) and that `@adonis-agora/telescope`'s generic diagnostics * watcher records — one entry per publish, stored as `type: 'diagnostic'`, `tag: 'lib:agent'`, with the * agent payload preserved verbatim under `content.payload`. * * No agent-specific watcher is contributed: capture is entirely handled by the generic bridge, so the * extension only has to SURFACE the recorded history. This mirrors `@adonis-agora/media/telescope`'s * entry-backed providers, and needs zero coupling to the agent container internals — no NestJS-style * DI/read-model resolution. */ /** * Newest-first cap on how many recorded agent entries a provider scans. Shared by every * `lib:agent` consumer of `fetchEntries` — including `rag-data-providers.ts`, since retrieval * events are recorded on this SAME generic slice (Adonis's diagnostics bridge has no per-event * entry type to give RAG its own window; see that file's header for what this implies). */ export const ENTRY_LIMIT = 5_000; /** Default rollup window: 24h. */ export const DAY_MS = 24 * 60 * 60 * 1000; const contentOf = (e) => (e.content ?? {}); /** Epoch millis of an entry's `createdAt`, or `0` when absent. Exported for `rag-data-providers.ts`. */ export const atOf = (e) => (e.createdAt ? +new Date(e.createdAt) : 0); const isoTime = (ms) => ms ? `${new Date(ms).toISOString().replace('T', ' ').slice(0, 19)}Z` : ''; /** Fetch captured `agora:agent:*` entries from Telescope storage (newest-first). */ export async function fetchEntries(ctx, limit = ENTRY_LIMIT) { return ctx.store.list({ type: 'diagnostic', tag: 'lib:agent', limit }); } /** A `tool-call` entry for a call that ran, not one re-published when the journal replayed it. */ function isFreshToolCall(e) { const content = contentOf(e); return content.event === 'tool-call' && content.payload?.replayed !== true; } function countEvent(entries, event) { let n = 0; for (const e of entries) if (contentOf(e).event === event) n += 1; return n; } /** Split entries into current `(now-window, now]` and previous `(now-2window, now-window]`. */ function splitWindows(entries, windowMs, now) { if (windowMs <= 0) return { current: entries, previous: [] }; const start = now - windowMs; const prevStart = start - windowMs; return { current: entries.filter((e) => atOf(e) > start && atOf(e) <= now), previous: entries.filter((e) => atOf(e) > prevStart && atOf(e) <= start), }; } /** * Build N equal-width time buckets spanning from the oldest entry to now, each starting empty. * Exported so `rag-data-providers.ts` buckets retrieval entries the same way the agent providers * bucket everything else, instead of a second, drifting bucketing scheme. */ export function timeBuckets(entries, count, emptyRow) { const now = Date.now(); let minTime = now; for (const e of entries) { const at = atOf(e) || now; if (at < minTime) minTime = at; } const span = Math.max(now - minTime, 1); const bucketSize = span / count; const rows = Array.from({ length: count }, (_, i) => ({ label: new Date(minTime + i * bucketSize).toISOString().slice(11, 16), ...emptyRow(), })); return { rows, minTime, bucketSize }; } /** Which of `timeBuckets`'s N buckets `e` falls into, clamped to `[0, count-1]`. */ export function bucketIndexFor(e, minTime, bucketSize, count) { const at = atOf(e) || minTime; return Math.min(count - 1, Math.max(0, Math.floor((at - minTime) / bucketSize))); } // ─── Runs ─────────────────────────────────────────────────────────────────── /** * Net in-flight runs = `run.started` − every ending (`run.finished`, `run.failed`, `run.cancelled`) * over the window (default 24h; `windowMs: 0` = all-time). Floored at 0. A cheap "how many agent runs * are live right now" stat derived purely from the captured event stream — no live run store is * consulted. */ export function agentActiveRunsProvider() { return { name: 'agent.activeRuns', async resolve(query, ctx) { const windowMs = Number(query?.windowMs ?? DAY_MS); const { current } = splitWindows(await fetchEntries(ctx), windowMs, Date.now()); const started = countEvent(current, 'run.started'); const ended = countEvent(current, 'run.finished') + countEvent(current, 'run.failed') + countEvent(current, 'run.cancelled'); return { value: Math.max(0, started - ended) }; }, }; } /** * Token usage = total (input + output) tokens summed from `run.finished` over the window (default 24h), * with `delta` vs the prior window and an 8-point spark. The entry-backed proxy for "spend/usage" — no * pricing read-model is joined (that would need host DI, which this extension deliberately avoids). */ export function agentTokenUsageProvider() { return { name: 'agent.tokenUsage', async resolve(query, ctx) { const windowMs = Number(query?.windowMs ?? DAY_MS); const now = Date.now(); const { current, previous } = splitWindows(await fetchEntries(ctx), windowMs, now); const tokensOf = (list) => { let total = 0; for (const e of list) { if (contentOf(e).event !== 'run.finished') continue; const p = contentOf(e).payload ?? {}; total += (p.inputTokens ?? 0) + (p.outputTokens ?? 0); } return total; }; const value = tokensOf(current); const delta = previous.length > 0 ? value - tokensOf(previous) : undefined; const sparkBuckets = 8; const bucketMs = (windowMs > 0 ? windowMs : Math.max(now, 1)) / sparkBuckets; const start = now - (windowMs > 0 ? windowMs : now); const spark = Array.from({ length: sparkBuckets }, (_, i) => { const from = start + i * bucketMs; return tokensOf(current.filter((e) => atOf(e) > from && atOf(e) <= from + bucketMs)); }); return delta === undefined ? { value, spark } : { value, delta, spark }; }, }; } /** * Tool-call success rate = executed / (executed + rejected + failed) over the window; 1 when no data. * `rejected` counts an approval that was denied — a first-class governance signal, not a crash. */ export function agentToolCallSuccessRateProvider() { return { name: 'agent.toolCallSuccessRate', async resolve(query, ctx) { const windowMs = Number(query?.windowMs ?? DAY_MS); const { current } = splitWindows(await fetchEntries(ctx), windowMs, Date.now()); let executed = 0; let other = 0; for (const e of current) { if (!isFreshToolCall(e)) continue; const status = contentOf(e).payload?.status; if (status === 'executed') executed += 1; else if (status === 'rejected' || status === 'failed') other += 1; } const total = executed + other; return { value: total === 0 ? 1 : executed / total, min: 0, max: 1 }; }, }; } /** Runs over time — `run.started` (started) vs `run.finished` (finished) per bucket. */ export function agentRunsOverTimeProvider() { return { name: 'agent.runsOverTime', async resolve(query, ctx) { const entries = await fetchEntries(ctx); const buckets = Math.max(1, Number(query?.buckets ?? 24)); const { rows, minTime, bucketSize } = timeBuckets(entries, buckets, () => ({ started: 0, finished: 0, })); for (const e of entries) { const event = contentOf(e).event; if (event !== 'run.started' && event !== 'run.finished') continue; const row = rows[bucketIndexFor(e, minTime, bucketSize, buckets)]; if (!row) continue; if (event === 'run.started') row.started += 1; else row.finished += 1; } return { rows }; }, }; } /** Token throughput over time — input/output tokens per bucket, summed from `run.finished`. */ export function agentTokensOverTimeProvider() { return { name: 'agent.tokensOverTime', async resolve(query, ctx) { const entries = await fetchEntries(ctx); const buckets = Math.max(1, Number(query?.buckets ?? 24)); const { rows, minTime, bucketSize } = timeBuckets(entries, buckets, () => ({ input: 0, output: 0, })); for (const e of entries) { if (contentOf(e).event !== 'run.finished') continue; const row = rows[bucketIndexFor(e, minTime, bucketSize, buckets)]; if (!row) continue; const p = contentOf(e).payload ?? {}; row.input += p.inputTokens ?? 0; row.output += p.outputTokens ?? 0; } return { rows }; }, }; } /** Recent finished runs (newest first) as table rows: time, run, thread, steps, tokens. */ export function agentRecentRunsProvider() { return { name: 'agent.recentRuns', async resolve(query, ctx) { const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50))); const rows = (await fetchEntries(ctx)) .filter((e) => contentOf(e).event === 'run.finished') .sort((a, b) => atOf(b) - atOf(a)) .slice(0, limit) .map((e) => { const p = contentOf(e).payload ?? {}; return { time: isoTime(atOf(e)), runId: p.runId ?? '', thread: p.threadId ?? '', steps: p.steps ?? 0, tokens: (p.inputTokens ?? 0) + (p.outputTokens ?? 0), }; }); return { rows }; }, }; } // ─── Run latency & failures ────────────────────────────────────────────────── /** Equal-width buckets a distribution panel draws. */ const DISTRIBUTION_BUCKETS = 8; const durationLabel = (ms) => ms < 1000 ? `${Math.round(ms)}ms` : `${Math.round(ms / 100) / 10}s`; /** * Turn latency as a distribution, from `run.finished` over the window (default 24h; `windowMs: 0` = * all-time): `query.metric` `'ttft'` (default — turn start → first text frame; a turn that streamed * no text has no sample) or `'duration'` (the whole turn). A delegation's child run is left out: it is * part of its parent's turn, the same rule the read-model's `RunReliability.latency` follows. * Percentiles are nearest-rank and omitted when there are no samples. */ export function agentLatencyDistributionProvider() { return { name: 'agent.latency', async resolve(query, ctx) { const windowMs = Number(query?.windowMs ?? DAY_MS); const metric = query?.metric === 'duration' ? 'durationMs' : 'ttftMs'; const { current } = splitWindows(await fetchEntries(ctx), windowMs, Date.now()); const samples = []; for (const e of current) { const content = contentOf(e); if (content.event !== 'run.finished' || content.payload?.parentRunId !== undefined) continue; const value = content.payload?.[metric]; if (typeof value === 'number' && Number.isFinite(value)) samples.push(value); } samples.sort((a, b) => a - b); const max = samples.at(-1) ?? 0; const size = Math.max(1, Math.ceil((max + 1) / DISTRIBUTION_BUCKETS)); const buckets = Array.from({ length: DISTRIBUTION_BUCKETS }, (_, i) => ({ label: durationLabel(i * size), count: 0, })); for (const value of samples) { const bucket = buckets[Math.min(DISTRIBUTION_BUCKETS - 1, Math.floor(value / size))]; if (bucket) bucket.count += 1; } if (samples.length === 0) return { buckets }; return { buckets, p50: percentile(samples, 0.5), p95: percentile(samples, 0.95), p99: percentile(samples, 0.99), }; }, }; } /** * Failed runs by the stable code their stream `error` frame carried, from `run.failed` over the * window (default 24h; `windowMs: 0` = all-time), most frequent first. An entry without a code is * `unknown`. */ export function agentFailuresByCodeProvider() { return { name: 'agent.failuresByCode', async resolve(query, ctx) { const windowMs = Number(query?.windowMs ?? DAY_MS); const { current } = splitWindows(await fetchEntries(ctx), windowMs, Date.now()); const counts = new Map(); for (const e of current) { const content = contentOf(e); if (content.event !== 'run.failed') continue; const code = content.payload?.code ?? 'unknown'; counts.set(code, (counts.get(code) ?? 0) + 1); } return { segments: [...counts.entries()] .map(([label, value]) => ({ label, value })) .sort((a, b) => b.value - a.value || a.label.localeCompare(b.label)), }; }, }; } // ─── Tools & approvals ──────────────────────────────────────────────────────── /** Tool calls over time — executed/rejected/failed per bucket (the `tool-call` event). */ export function agentToolCallsOverTimeProvider() { return { name: 'agent.toolCallsOverTime', async resolve(query, ctx) { const entries = await fetchEntries(ctx); const buckets = Math.max(1, Number(query?.buckets ?? 24)); const { rows, minTime, bucketSize } = timeBuckets(entries, buckets, () => ({ executed: 0, rejected: 0, failed: 0, })); for (const e of entries) { if (!isFreshToolCall(e)) continue; const row = rows[bucketIndexFor(e, minTime, bucketSize, buckets)]; if (!row) continue; const status = contentOf(e).payload?.status; if (status === 'executed') row.executed += 1; else if (status === 'rejected') row.rejected += 1; else if (status === 'failed') row.failed += 1; } return { rows }; }, }; } /** Recent tool calls (newest first) as table rows: time, run, tool, type, status. */ export function agentRecentToolCallsProvider() { return { name: 'agent.recentToolCalls', async resolve(query, ctx) { const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50))); const rows = (await fetchEntries(ctx)) .filter((e) => contentOf(e).event === 'tool-call') .sort((a, b) => atOf(b) - atOf(a)) .slice(0, limit) .map((e) => { const p = contentOf(e).payload ?? {}; return { time: isoTime(atOf(e)), runId: p.runId ?? '', tool: p.toolName ?? '', type: p.toolType ?? '', status: p.status ?? '', }; }); return { rows }; }, }; } /** * Recent approval decisions (newest first): the `action`-type tool calls — the ones that pass through * the human-approval gate — surfaced with their outcome (`executed` = approved/auto, `rejected` = * denied). Read-type tool calls never require approval and are excluded. */ export function agentRecentApprovalsProvider() { return { name: 'agent.recentApprovals', async resolve(query, ctx) { const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50))); const rows = (await fetchEntries(ctx)) .filter((e) => contentOf(e).event === 'tool-call' && contentOf(e).payload?.toolType === 'action') .sort((a, b) => atOf(b) - atOf(a)) .slice(0, limit) .map((e) => { const p = contentOf(e).payload ?? {}; return { time: isoTime(atOf(e)), runId: p.runId ?? '', tool: p.toolName ?? '', status: p.status ?? '', }; }); return { rows }; }, }; } // ─── Delegations ──────────────────────────────────────────────────────────── /** Sub-agent delegations over time — the `delegated` event per bucket. */ export function agentDelegationsOverTimeProvider() { return { name: 'agent.delegationsOverTime', async resolve(query, ctx) { const entries = await fetchEntries(ctx); const buckets = Math.max(1, Number(query?.buckets ?? 24)); const { rows, minTime, bucketSize } = timeBuckets(entries, buckets, () => ({ delegations: 0, })); for (const e of entries) { if (contentOf(e).event !== 'delegated') continue; const row = rows[bucketIndexFor(e, minTime, bucketSize, buckets)]; if (row) row.delegations += 1; } return { rows }; }, }; } /** Recent delegations (newest first) as table rows: time, run, from, to. */ export function agentRecentDelegationsProvider() { return { name: 'agent.recentDelegations', async resolve(query, ctx) { const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50))); const rows = (await fetchEntries(ctx)) .filter((e) => contentOf(e).event === 'delegated') .sort((a, b) => atOf(b) - atOf(a)) .slice(0, limit) .map((e) => { const p = contentOf(e).payload ?? {}; return { time: isoTime(atOf(e)), runId: p.runId ?? '', from: p.fromAgent ?? '', to: p.toAgent ?? '', }; }); return { rows }; }, }; } //# sourceMappingURL=data-providers.js.map