UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

107 lines (106 loc) • 4.64 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * Aggregate `llm_usage` events (#576) into the window total + per-stage * breakdown reported by `akm health`. */ import { readEvents } from "../../core/events.js"; import { LLM_USAGE_EVENT } from "../../llm/usage-persist.js"; import { decodeLlmUsageRecord } from "../../llm/usage-telemetry.js"; /** Stage/process/engine/model key used for `llm_usage` events recorded with no value for that dimension. */ const UNATTRIBUTED_STAGE = "unattributed"; function emptyLlmUsageStageAggregate() { return { calls: 0, totalDurationMs: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, reasoningTokens: 0, failures: 0, }; } /** A zeroed aggregate — also the value health reports when it could not read state.db at all (#791). */ export function emptyLlmUsageAggregate() { return { ...emptyLlmUsageStageAggregate(), byStage: {}, byProcess: {}, byEngine: {} }; } /** * Aggregate `llm_usage` events (#576) into a window total plus a per-stage * breakdown of call count, wall-time, and token usage. Token fields absent from * a best-effort record contribute 0. Calls with no `stage` land under * {@link UNATTRIBUTED_STAGE}. */ export function summarizeLlmUsage(events) { const aggregate = emptyLlmUsageAggregate(); for (const event of events) { const record = decodeLlmUsageRecord(event.metadata); if (!record) continue; const dimensions = []; for (const [groups, key] of [ [aggregate.byStage, record.stage ?? UNATTRIBUTED_STAGE], [aggregate.byProcess, record.process ?? UNATTRIBUTED_STAGE], [aggregate.byEngine, record.engine ?? UNATTRIBUTED_STAGE], ]) { groups[key] ??= emptyLlmUsageStageAggregate(); dimensions.push(groups[key]); } for (const target of [aggregate, ...dimensions]) { target.calls += 1; target.totalDurationMs += record.durationMs; target.promptTokens += record.promptTokens ?? 0; target.completionTokens += record.completionTokens ?? 0; target.totalTokens += record.totalTokens ?? 0; target.reasoningTokens += record.reasoningTokens ?? 0; if (record.outcome === "error") target.failures += 1; } } return aggregate; } export function readLlmUsageAggregate(stateDbPath, since, until) { const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath }).events.filter((event) => { if (until === undefined) return true; return new Date(event.ts ?? since).getTime() < new Date(until).getTime(); }); return summarizeLlmUsage(events); } /** * Aggregate `llm_usage` events (#576) into a process x engine x model * cross-tab (#944) — one row per distinct `(process, engine, model)` triple * seen, each carrying the same call/failure/token/duration totals as * {@link LlmUsageStageAggregate}. A call missing any one of the three * dimensions is keyed under {@link UNATTRIBUTED_STAGE} for that dimension * (never dropped). Does not fold into, or change the shape of, * {@link summarizeLlmUsage}'s existing `byStage`/`byProcess`/`byEngine` * breakdowns — `akm health`'s existing consumers of those stay untouched. * Row order is insertion order (first `(process, engine, model)` triple seen). */ export function summarizeLlmUsageCrossTab(events) { const rows = new Map(); for (const event of events) { const record = decodeLlmUsageRecord(event.metadata); if (!record) continue; const process = record.process ?? UNATTRIBUTED_STAGE; const engine = record.engine ?? UNATTRIBUTED_STAGE; const model = record.model ?? UNATTRIBUTED_STAGE; const key = `${process}:${engine}:${model}`; let row = rows.get(key); if (!row) { row = { process, engine, model, ...emptyLlmUsageStageAggregate() }; rows.set(key, row); } row.calls += 1; row.totalDurationMs += record.durationMs; row.promptTokens += record.promptTokens ?? 0; row.completionTokens += record.completionTokens ?? 0; row.totalTokens += record.totalTokens ?? 0; row.reasoningTokens += record.reasoningTokens ?? 0; if (record.outcome === "error") row.failures += 1; } return [...rows.values()]; }