Files
fusion/packages/core/src/agent-token-usage.ts
gsxdsm 100164dc50 FN-7150: show ephemeral agent token counts
Surface task-derived token usage for ephemeral agents in dashboard agent views.

- Attribute task token totals across assigned, source, and checkout agent links.
- Backfill zero-valued agent list token totals from linked task usage.
- Allow Agent Detail token usage windows for ephemeral/task-worker agents and cover the routes with tests.
- Document the dashboard behavior and add a patch changeset.

Files changed:
 .changeset/fn-7150-ephemeral-agent-token-counts.md |  7 ++
 docs/dashboard-guide.md                            |  1 +
 .../core/src/__tests__/agent-token-usage.test.ts   | 99 ++++++++++++++++++++--
 packages/core/src/__tests__/team-analytics.test.ts | 48 +++++++++++
 packages/core/src/agent-token-usage.ts             | 83 +++++++++++++++++-
 packages/core/src/index.ts                         |  4 +-
 .../__tests__/AgentTokenStatsPanel.test.tsx        | 21 +++++
 .../src/__tests__/routes-agent-token-usage.test.ts | 20 ++++-
 .../dashboard/src/__tests__/routes-agents.test.ts  | 87 +++++++++++++++++++
 .../src/routes/register-agent-core-routes.ts       | 38 ++++++++-
 .../src/routes/register-agent-runtime-routes.ts    |  5 +-
 11 files changed, 395 insertions(+), 18 deletions(-)

Fusion-Task-Id: FN-7150
Fusion-Task-Lineage: ce2661ce-5ba5-43b0-aa5c-7fddee90a915
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-06-27 19:37:39 -07:00

186 lines
5.8 KiB
TypeScript

import type { AgentStore } from "./agent-store.js";
import type { Database } from "./db.js";
import type { TaskStore } from "./store.js";
import type { AgentRole } from "./types.js";
export interface AgentTokenUsageWindowSummary {
totalInputTokens: number;
totalCachedTokens: number;
totalCacheWriteTokens: number;
totalOutputTokens: number;
nTasks: number;
hitRatio: number;
}
export interface AgentTokenUsageSummary {
agentId: string;
role: AgentRole;
last24h: AgentTokenUsageWindowSummary;
last7d: AgentTokenUsageWindowSummary;
allTime: AgentTokenUsageWindowSummary;
}
export interface AgentTaskTokenTotals {
inputTokens: number;
cachedTokens: number;
cacheWriteTokens: number;
outputTokens: number;
totalTokens: number;
nTasks: number;
}
interface TaskTokenLinkRow {
taskId: string;
assignedAgentId: string | null;
sourceAgentId: string | null;
checkedOutBy: string | null;
inputTokens: number | null;
cachedTokens: number | null;
cacheWriteTokens: number | null;
outputTokens: number | null;
totalTokens: number | null;
}
export function aggregateTaskTokenTotalsByAgentLink(db: Database): Map<string, AgentTaskTokenTotals> {
/*
FNXC:AgentTokenUsage 2026-06-27-23:06:
List-row token totals must use the same assigned/source/checkout attribution as Agent Detail so ephemeral task-worker agents do not report zero when they only sourced or checked out a task.
*/
const rows = db.prepare(`
SELECT
id AS taskId,
assignedAgentId,
sourceAgentId,
checkedOutBy,
tokenUsageInputTokens AS inputTokens,
tokenUsageCachedTokens AS cachedTokens,
tokenUsageCacheWriteTokens AS cacheWriteTokens,
tokenUsageOutputTokens AS outputTokens,
tokenUsageTotalTokens AS totalTokens
FROM tasks
WHERE tokenUsageInputTokens IS NOT NULL
OR tokenUsageCachedTokens IS NOT NULL
OR tokenUsageCacheWriteTokens IS NOT NULL
OR tokenUsageOutputTokens IS NOT NULL
OR tokenUsageTotalTokens IS NOT NULL
`).all() as TaskTokenLinkRow[];
const totalsByAgentId = new Map<string, AgentTaskTokenTotals>();
for (const row of rows) {
const agentIds = new Set([row.assignedAgentId, row.sourceAgentId, row.checkedOutBy].filter((value): value is string => Boolean(value)));
for (const agentId of agentIds) {
const existing = totalsByAgentId.get(agentId) ?? createTaskTokenTotals();
existing.inputTokens += row.inputTokens ?? 0;
existing.cachedTokens += row.cachedTokens ?? 0;
existing.cacheWriteTokens += row.cacheWriteTokens ?? 0;
existing.outputTokens += row.outputTokens ?? 0;
existing.totalTokens += row.totalTokens ?? (row.inputTokens ?? 0) + (row.cachedTokens ?? 0) + (row.cacheWriteTokens ?? 0) + (row.outputTokens ?? 0);
existing.nTasks += 1;
totalsByAgentId.set(agentId, existing);
}
}
return totalsByAgentId;
}
function createTaskTokenTotals(): AgentTaskTokenTotals {
return {
inputTokens: 0,
cachedTokens: 0,
cacheWriteTokens: 0,
outputTokens: 0,
totalTokens: 0,
nTasks: 0,
};
}
export async function aggregateAgentTokenUsage({
taskStore,
agentStore,
agentId,
now = new Date(),
}: {
taskStore: TaskStore;
agentStore: AgentStore;
agentId: string;
now?: Date;
}): Promise<AgentTokenUsageSummary | null> {
const agent = await agentStore.getAgent(agentId);
if (!agent) {
return null;
}
/*
FNXC:AgentTokenUsage 2026-06-27-19:10:
Ephemeral/task-worker agents must surface task-derived token usage because their cumulative agent token fields are never accumulated by the durable-agent heartbeat path.
*/
const tasks = await taskStore.listTasks({ slim: true, includeArchived: true });
const nowMs = now.getTime();
const last24hMs = nowMs - (24 * 60 * 60 * 1000);
const last7dMs = nowMs - (7 * 24 * 60 * 60 * 1000);
const allTime = createWindowSummary();
const last24h = createWindowSummary();
const last7d = createWindowSummary();
for (const task of tasks) {
if (!task.tokenUsage) continue;
const matchesAgent = task.assignedAgentId === agentId || task.sourceAgentId === agentId || task.checkedOutBy === agentId;
if (!matchesAgent) continue;
const usage = task.tokenUsage;
applyTaskUsage(allTime, usage.inputTokens ?? 0, usage.cachedTokens ?? 0, usage.outputTokens ?? 0, usage.cacheWriteTokens ?? 0);
const lastUsedAtMs = Date.parse(usage.lastUsedAt ?? "");
if (!Number.isFinite(lastUsedAtMs)) continue;
if (lastUsedAtMs >= last24hMs) {
applyTaskUsage(last24h, usage.inputTokens ?? 0, usage.cachedTokens ?? 0, usage.outputTokens ?? 0, usage.cacheWriteTokens ?? 0);
}
if (lastUsedAtMs >= last7dMs) {
applyTaskUsage(last7d, usage.inputTokens ?? 0, usage.cachedTokens ?? 0, usage.outputTokens ?? 0, usage.cacheWriteTokens ?? 0);
}
}
return {
agentId,
role: agent.role as AgentRole,
last24h: finalizeWindowSummary(last24h),
last7d: finalizeWindowSummary(last7d),
allTime: finalizeWindowSummary(allTime),
};
}
function createWindowSummary(): AgentTokenUsageWindowSummary {
return {
totalInputTokens: 0,
totalCachedTokens: 0,
totalCacheWriteTokens: 0,
totalOutputTokens: 0,
nTasks: 0,
hitRatio: 0,
};
}
function applyTaskUsage(
summary: AgentTokenUsageWindowSummary,
inputTokens: number,
cachedTokens: number,
outputTokens: number,
cacheWriteTokens: number,
): void {
summary.totalInputTokens += inputTokens;
summary.totalCachedTokens += cachedTokens;
summary.totalCacheWriteTokens += cacheWriteTokens;
summary.totalOutputTokens += outputTokens;
summary.nTasks += 1;
}
function finalizeWindowSummary(summary: AgentTokenUsageWindowSummary): AgentTokenUsageWindowSummary {
const denominator = summary.totalInputTokens + summary.totalCachedTokens;
return {
...summary,
hitRatio: denominator > 0 ? summary.totalCachedTokens / denominator : 0,
};
}