+
{modelLabel != null ? (
-
-
- {modelLabel}
+
+ {modelLabel}
- ) : null}
- {skillsCount > 0 ? (
-
-
-
- {skillsCount}
-
-
- ) : null}
- {mcpCount > 0 ? (
-
-
-
- {mcpCount}
-
-
- ) : null}
+ ) : (
+ —
+ )}
+ {mcpCount === 0 && skillsCount === 0 ? (
+ no tools
+ ) : (
+
+ {mcpCount > 0 ? (
+
+
+ {mcpCount} MCP
+
+
+ ) : null}
+ {mcpCount > 0 && skillsCount > 0 ? • : null}
+ {skillsCount > 0 ? (
+
+
+ {skillsCount} skill{skillsCount === 1 ? '' : 's'}
+
+
+ ) : null}
+
+ )}
);
}
@@ -336,8 +366,10 @@ export function AgentLibraryRow({
canManageAgent = true,
canDeleteAgent = true,
canManageSchedules,
- showCreatedBy = false,
scheduleSummary,
+ usage,
+ showMetricsColumns = false,
+ showLastRunColumn = false,
onOpenSchedules,
onCreateSchedule,
onOpen,
@@ -347,22 +379,29 @@ export function AgentLibraryRow({
variant = 'row',
}: AgentLibraryRowProps) {
const PermissionGuard = useSlot('PermissionGuard');
+ const SlottedUsageCells = useSlot('AgentLibraryUsageCells');
const spec = agent.agentSpec;
const modelName = spec?.model.name;
- const skillsCount = spec?.skills?.length ?? 0;
- const mcpCount = spec?.mcpServers?.length ?? 0;
const skillNames = (spec?.skills ?? []).map(mountName).filter((name: string | null): name is string => name != null);
const mcpNames = (spec?.mcpServers ?? [])
.map(mountName)
.filter((name: string | null): name is string => name != null);
const modelLabel = modelName != null ? displayModelLabel(modelName) : null;
const modelTitle = modelName ?? '';
- const connectorsTitle = mcpNames.length ? mcpNames.join(', ') : `${mcpCount} connectors`;
- const skillsTitle = skillNames.length ? skillNames.join(', ') : `${skillsCount} skills`;
const hasNoSchedules = scheduleSummary != null && scheduleSummary.count === 0;
const storedDescription = agent.description?.trim() || null;
// Create falls back to name when description is missing; don't echo it under the title.
const description = storedDescription != null && storedDescription !== agent.name ? storedDescription : null;
+ const usageLoading = (showMetricsColumns || showLastRunColumn) && usage == null;
+ const usageCells = (
+
+ );
const overflowMenu = (
);
- const configurationChips = (
-
+ );
+
+ const nameContent = (
+
);
@@ -404,27 +450,11 @@ export function AgentLibraryRow({
return (
-
+
{nameContent}
{overflowMenu}
-
- {showCreatedBy ? : null}
+ {configurationSummary}
+ {usageCells}
{/* Fixed width so truncate works; 24rem = 1.5× the prior min-w-64 name column. */}
-
-
-
- {configurationChips}
- {showCreatedBy ? (
-
-
-
- ) : null}
+ {nameContent}
+ {configurationSummary}
{scheduleSummary !== undefined ? (
) : null}
+ {showMetricsColumns ? (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ ) : null}
-
+
+ {showLastRunColumn ?
: null}
{tryButton}
{overflowMenu}
@@ -585,13 +622,21 @@ export function AgentsLibrary({ onSelectAgent, headerStart }: AgentsLibraryProps
limit: DEFAULT_TABLE_PAGE_SIZE,
});
const hasPageNav = canPrev || canNext;
- const showCreatedByColumn = hasCreatedBySubject(agents);
const permissionAgentIds = open ? agents.map(libraryAgentId) : [];
const { allows } = useResourcePermissions({
resourceType: 'agent',
resourceIds: permissionAgentIds,
});
+ const {
+ usageByAgentId,
+ metricsEnabled: showMetricsColumns,
+ lastRunEnabled: showLastRunColumn,
+ } = useAgentLibraryUsageStats({
+ agentIds: permissionAgentIds,
+ refreshKey: agentsListEpoch,
+ });
+
useEffect(() => {
if (!open || scheduleServer == null || agents.length === 0) {
setScheduleByAgent(null);
@@ -664,6 +709,7 @@ export function AgentsLibrary({ onSelectAgent, headerStart }: AgentsLibraryProps
scheduleByAgent?.get(agent.name) ??
(scheduleByAgent == null ? null : { count: 0, pausedCount: 0, activeNames: [], pausedNames: [] }))
: undefined;
+ const usage = usageByAgentId.get(id);
return (
{Array.from({ length: 6 }, (_, i) => (
-
+
))}
) : error ? (
@@ -810,12 +858,20 @@ export function AgentsLibrary({ onSelectAgent, headerStart }: AgentsLibraryProps
- Agent name
+ Agent
Configuration
- {showCreatedByColumn ? Created by : null}
{showSchedulesColumn ? Schedules : null}
+ {showMetricsColumns ? (
+ <>
+ {agentLibraryUsageLabels.sessions}
+ {agentLibraryUsageLabels.spend}
+ {agentLibraryUsageLabels.daily}
+ >
+ ) : null}
- Actions
+
+ {showLastRunColumn ? 'Last run' : 'Actions'}
+
diff --git a/packages/trueforge-ui/src/atoms/lib/useAgentLibraryUsageStats.ts b/packages/trueforge-ui/src/atoms/lib/useAgentLibraryUsageStats.ts
new file mode 100644
index 000000000..7790e0a49
--- /dev/null
+++ b/packages/trueforge-ui/src/atoms/lib/useAgentLibraryUsageStats.ts
@@ -0,0 +1,210 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+
+import { useOptionalAgentMetricsServer, useOptionalAgentSessionsServer } from '../../server/ServerContext.js';
+import type {
+ AgentMetricMeter,
+ AgentMetricPoint,
+ AgentMetricsServer,
+ AgentSessionsServer,
+} from '../../server/types.js';
+import { DEFAULT_SESSION_TIME_WINDOW_MS } from '../../utils/sessionShareUrl.js';
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+/** Daily cost series length. Meters use the session time window below. */
+export const AGENT_LIBRARY_SPARKLINE_DAYS = 14;
+/** Day count of `DEFAULT_SESSION_TIME_WINDOW_MS`, which the library meters query uses. */
+export const AGENT_LIBRARY_METERS_WINDOW_DAYS = DEFAULT_SESSION_TIME_WINDOW_MS / MS_PER_DAY;
+const SPARKLINE_WINDOW_MS = AGENT_LIBRARY_SPARKLINE_DAYS * MS_PER_DAY;
+const CHART_NAME = 'sessions_cost_over_time';
+
+export type AgentLibraryDailyPoint = {
+ timestamp: string;
+ value: number;
+};
+
+export type AgentLibraryUsageStats = {
+ totalSessions: number | null;
+ totalCostUsd: number | null;
+ costPerSessionUsd: number | null;
+ dailyPoints: AgentLibraryDailyPoint[] | null;
+ /**
+ * Present only after `listSessions` succeeds. `null` means the agent has no
+ * sessions; omitted when the fetch failed (distinct from "never ran").
+ */
+ lastRunAt?: string | null;
+};
+
+function meterValue(meters: readonly AgentMetricMeter[], name: string): number | null {
+ const match = meters.find(meter => meter.name === name);
+ return match == null || !Number.isFinite(match.aggregateValue) ? null : match.aggregateValue;
+}
+
+function chartPoints(points: readonly AgentMetricPoint[] | undefined): AgentLibraryDailyPoint[] | null {
+ if (points == null) return null;
+ return points.map(point => ({
+ timestamp: point.timestamp,
+ value: Number.isFinite(point.value) ? point.value : 0,
+ }));
+}
+
+function unavailableUsage(): AgentLibraryUsageStats {
+ return {
+ totalSessions: null,
+ totalCostUsd: null,
+ costPerSessionUsd: null,
+ dailyPoints: null,
+ };
+}
+
+async function loadUsageForAgent({
+ agentId,
+ metrics,
+ sessions,
+ metersStart,
+ metersEnd,
+ sparklineStart,
+ sparklineEnd,
+}: {
+ agentId: string;
+ metrics: AgentMetricsServer | null;
+ sessions: AgentSessionsServer | null;
+ metersStart: string;
+ metersEnd: string;
+ sparklineStart: string;
+ sparklineEnd: string;
+}): Promise {
+ const metersPromise =
+ metrics == null
+ ? Promise.resolve({ ok: false as const })
+ : metrics
+ .getMeters({
+ agentId,
+ startTimestamp: metersStart,
+ endTimestamp: metersEnd,
+ })
+ .then(
+ meters => ({ ok: true as const, meters }),
+ () => ({ ok: false as const }),
+ );
+
+ const chartPromise =
+ metrics == null
+ ? Promise.resolve({ ok: false as const })
+ : metrics
+ .getChartData({
+ agentId,
+ startTimestamp: sparklineStart,
+ endTimestamp: sparklineEnd,
+ chartName: CHART_NAME,
+ })
+ .then(
+ data => ({ ok: true as const, data }),
+ () => ({ ok: false as const }),
+ );
+
+ // `undefined` = sessions server absent or the call failed; `null` = no sessions.
+ const lastRunPromise: Promise =
+ sessions == null
+ ? Promise.resolve(undefined)
+ : sessions.listSessions({ agentId, limit: 1, order: 'desc' }).then(
+ page => page.data[0]?.lastActivityAt ?? null,
+ () => undefined,
+ );
+
+ const [metersResult, chartResult, lastRunAt] = await Promise.all([metersPromise, chartPromise, lastRunPromise]);
+
+ const meters = metersResult.ok ? metersResult.meters : [];
+ const dailyPoints = chartResult.ok ? chartPoints(chartResult.data.graphs[0]?.graphLines[0]?.values) : null;
+
+ return {
+ totalSessions: metersResult.ok ? meterValue(meters, 'total_sessions') : null,
+ totalCostUsd: metersResult.ok ? meterValue(meters, 'total_cost_in_usd') : null,
+ costPerSessionUsd: metersResult.ok ? meterValue(meters, 'cost_per_session_in_usd') : null,
+ dailyPoints,
+ ...(lastRunAt === undefined ? {} : { lastRunAt }),
+ };
+}
+
+/**
+ * Loads per-agent spend/session meters, a 14d cost sparkline, and last-run
+ * for the visible library page. Each agent paints as its own requests finish.
+ * ponytail: three requests per agent (N+1). Replace with one batched metrics
+ * call if the library page grows past a few dozen rows.
+ */
+export function useAgentLibraryUsageStats({
+ agentIds,
+ refreshKey,
+}: {
+ agentIds: readonly string[];
+ refreshKey: number;
+}): {
+ usageByAgentId: Map;
+ metricsEnabled: boolean;
+ lastRunEnabled: boolean;
+} {
+ const metrics = useOptionalAgentMetricsServer();
+ const sessions = useOptionalAgentSessionsServer();
+ const metricsEnabled = metrics != null;
+ const lastRunEnabled = sessions != null;
+ const [usageByAgentId, setUsageByAgentId] = useState