Capture per-session token usage from pi-coding-agent's getSessionStats() after each promptWithFallback in the executor and merger paths, so task.tokenUsage populates live during runs and reflects final totals on done tasks. Previously the executor never read session usage and only the heartbeat path bumped agent token totals, leaving task.tokenUsage undefined even after completion. Stats panel and done-card timing also now reflect live state: the modal overlays the SSE-updated task prop on top of the one-shot fullDetail snapshot, in-progress workflow steps contribute live elapsed to the Workflow runtime metric, and the done card uses Timed duration (matching the stats tab) with workflow runtime as fallback. Time indicator labels coarsened to <1m / Nm / Nh / Nd. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import type { TaskStore } from "@fusion/core";
|
|
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
import { createLogger } from "./logger.js";
|
|
|
|
const log = createLogger("session-token-usage");
|
|
|
|
interface SessionBaseline {
|
|
input: number;
|
|
output: number;
|
|
cached: number;
|
|
}
|
|
|
|
// Per-session cumulative-token baselines so repeated calls only persist deltas.
|
|
// The session object is keyed weakly so disposed sessions get garbage-collected.
|
|
const sessionBaselines = new WeakMap<AgentSession, SessionBaseline>();
|
|
|
|
interface SessionStatsLike {
|
|
tokens?: {
|
|
input?: number;
|
|
output?: number;
|
|
cacheRead?: number;
|
|
cacheWrite?: number;
|
|
};
|
|
}
|
|
|
|
function readSessionStats(session: AgentSession): SessionStatsLike | undefined {
|
|
const accessor = (session as unknown as { getSessionStats?: () => SessionStatsLike }).getSessionStats;
|
|
if (typeof accessor !== "function") return undefined;
|
|
try {
|
|
return accessor.call(session);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Capture the session's cumulative token usage and accumulate any *new* deltas
|
|
* onto `task.tokenUsage`. Safe to call repeatedly on the same session — each
|
|
* call only persists what's been added since the previous call (per-session
|
|
* baseline tracking). Failures are logged and swallowed so token bookkeeping
|
|
* never blocks the task pipeline.
|
|
*/
|
|
export async function accumulateSessionTokenUsage(
|
|
store: TaskStore,
|
|
taskId: string,
|
|
session: AgentSession,
|
|
): Promise<void> {
|
|
try {
|
|
const stats = readSessionStats(session);
|
|
const tokens = stats?.tokens;
|
|
if (!tokens) return;
|
|
|
|
// Treat cache-write tokens as input (they're billed as input on first write
|
|
// and read back at a discount on subsequent turns).
|
|
const currentInput = (tokens.input ?? 0) + (tokens.cacheWrite ?? 0);
|
|
const currentOutput = tokens.output ?? 0;
|
|
const currentCached = tokens.cacheRead ?? 0;
|
|
|
|
const baseline = sessionBaselines.get(session) ?? { input: 0, output: 0, cached: 0 };
|
|
const inputDelta = Math.max(0, currentInput - baseline.input);
|
|
const outputDelta = Math.max(0, currentOutput - baseline.output);
|
|
const cachedDelta = Math.max(0, currentCached - baseline.cached);
|
|
|
|
sessionBaselines.set(session, {
|
|
input: currentInput,
|
|
output: currentOutput,
|
|
cached: currentCached,
|
|
});
|
|
|
|
if (inputDelta === 0 && outputDelta === 0 && cachedDelta === 0) return;
|
|
|
|
const task = await store.getTask(taskId);
|
|
const now = new Date().toISOString();
|
|
const newInput = (task.tokenUsage?.inputTokens ?? 0) + inputDelta;
|
|
const newOutput = (task.tokenUsage?.outputTokens ?? 0) + outputDelta;
|
|
const newCached = (task.tokenUsage?.cachedTokens ?? 0) + cachedDelta;
|
|
|
|
await store.updateTask(taskId, {
|
|
tokenUsage: {
|
|
inputTokens: newInput,
|
|
outputTokens: newOutput,
|
|
cachedTokens: newCached,
|
|
totalTokens: newInput + newOutput + newCached,
|
|
firstUsedAt: task.tokenUsage?.firstUsedAt ?? now,
|
|
lastUsedAt: now,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
log.warn(`${taskId}: session token usage accumulate failed: ${message}`);
|
|
}
|
|
}
|