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
155 lines (154 loc) • 7.68 kB
JavaScript
// 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/.
/**
* #944 Layer 2 — `akm improve report`. Reads one (or several, `--since`)
* `improve_runs` rows and returns the same `usageReport` shape
* `finalizeImproveResult` persists on a live run (`improve-usage-report.ts`),
* so the command and the end-of-run stderr table stay byte-identical in
* structure. A row with no persisted `usageReport` degrades rather than
* errors, per AGENTS.md's "reader must tolerate data older releases wrote":
* its cross-tab is recomputed from the run's own `llm_usage` events and a
* `notes` entry explains why — either the row predates #944, or (per
* `recordTerminatedImproveRun` in improve-result-file.ts) the run was
* terminated before it ever reached `finalizeImproveResult`. The `noCalls`
* half is never fabricated for either case.
*/
import { NotFoundError, UsageError } from "../../core/errors.js";
import { readEvents } from "../../core/events.js";
import { decodeImproveResult } from "../../core/improve-result.js";
import { withStateDb } from "../../core/state-db.js";
import { LLM_USAGE_EVENT } from "../../llm/usage-persist.js";
import { getImproveRunById, getLatestImproveRun, queryImproveRuns, } from "../../storage/repositories/improve-runs-repository.js";
import { parseHealthSince } from "../health.js";
import { summarizeLlmUsageCrossTab } from "../health/llm-usage.js";
const PRE_USAGE_REPORT_NOTE = "eligibility reasons unavailable for runs recorded before 0.9.15";
const TERMINATED_RUN_NOTE = "run terminated before completion; no usage report was recorded";
/**
* Recompute the cross-tab from this run's own `llm_usage` events (a pre-#944
* row has no persisted one). `eventsCtx` threads the state.db connection
* `runImproveReportQuery` already holds via `withStateDb`, so a `--since`
* window with several pre-0.9.15 or undecodable rows reads through that one
* open connection instead of each row opening and closing its own.
*/
function crossTabFromEvents(startedAt, completedAt, eventsCtx) {
const until = completedAt ?? new Date().toISOString();
const events = readEvents({ since: startedAt, type: LLM_USAGE_EVENT }, eventsCtx).events.filter((event) => new Date(event.ts ?? startedAt).getTime() < new Date(until).getTime());
return summarizeLlmUsageCrossTab(events);
}
/** One row's usage report — persisted (common case) or recomputed (pre-#944 row / terminated run / undecodable row). */
function usageReportForRow(row, eventsCtx) {
try {
const decoded = decodeImproveResult(row.result_json);
if (decoded.envelope.usageReport) {
return {
usageReport: {
byProcessEngineModel: [...decoded.envelope.usageReport.byProcessEngineModel],
noCalls: [...decoded.envelope.usageReport.noCalls],
},
strategy: decoded.strategy,
};
}
return {
usageReport: {
byProcessEngineModel: crossTabFromEvents(row.started_at, row.completed_at, eventsCtx),
noCalls: [],
},
strategy: decoded.strategy,
note: decoded.envelope.terminated ? TERMINATED_RUN_NOTE : PRE_USAGE_REPORT_NOTE,
};
}
catch {
// A row whose result_json this build cannot decode still has real
// llm_usage events on state.db — degrade to the recomputed cross-tab
// rather than excluding the run entirely.
return {
usageReport: {
byProcessEngineModel: crossTabFromEvents(row.started_at, row.completed_at, eventsCtx),
noCalls: [],
},
note: PRE_USAGE_REPORT_NOTE,
};
}
}
function mergeCrossTabRow(merged, row) {
const key = `${row.process}:${row.engine}:${row.model}`;
const acc = merged.get(key);
if (!acc) {
merged.set(key, { ...row });
return;
}
acc.calls += row.calls;
acc.failures += row.failures;
acc.promptTokens += row.promptTokens;
acc.completionTokens += row.completionTokens;
acc.totalTokens += row.totalTokens;
acc.reasoningTokens += row.reasoningTokens;
acc.totalDurationMs += row.totalDurationMs;
}
/**
* Resolve `akm improve report`'s target run(s) and build its `usageReport`.
* `runId` and `since` are mutually exclusive; neither given selects the most
* recent non-dry-run.
*/
export function runImproveReportQuery(options) {
if (options.runId !== undefined && options.since !== undefined) {
throw new UsageError("akm improve report: --run and --since are mutually exclusive.", "INVALID_FLAG_VALUE");
}
return withStateDb((db) => {
// Thread this already-open connection into every reader below, so a
// `--since` window with several pre-0.9.15 or undecodable rows reads
// through it instead of each row opening and closing its own.
const eventsCtx = { db };
if (options.since !== undefined) {
const sinceIso = parseHealthSince(options.since);
const rows = queryImproveRuns(db, sinceIso);
const merged = new Map();
const calledEverByProcess = new Set();
const lastReasonByProcess = new Map();
const notes = new Set();
for (const row of rows) {
const { usageReport, note } = usageReportForRow(row, eventsCtx);
for (const crossTabRow of usageReport.byProcessEngineModel) {
mergeCrossTabRow(merged, crossTabRow);
if (crossTabRow.calls > 0)
calledEverByProcess.add(crossTabRow.process);
}
for (const noCallRow of usageReport.noCalls) {
// `rows` is newest-first (ORDER BY started_at DESC): keep only the
// first (most recent) reason seen per process, never overwrite it
// with an older run's reason.
if (!lastReasonByProcess.has(noCallRow.process)) {
lastReasonByProcess.set(noCallRow.process, { engine: noCallRow.engine, reason: noCallRow.reason });
}
}
if (note)
notes.add(note);
}
const noCalls = [...lastReasonByProcess.entries()]
.filter(([process]) => !calledEverByProcess.has(process))
.map(([process, { engine, reason }]) => ({ process, ...(engine ? { engine } : {}), reason }));
return {
mode: "since",
since: sinceIso,
runIds: rows.map((r) => r.id),
usageReport: { byProcessEngineModel: [...merged.values()], noCalls },
...(notes.size > 0 ? { notes: [...notes] } : {}),
};
}
const row = options.runId !== undefined ? getImproveRunById(db, options.runId) : getLatestImproveRun(db);
if (!row) {
throw new NotFoundError(options.runId !== undefined
? `akm improve report: no improve run found with id "${options.runId}".`
: "akm improve report: no improve runs recorded yet.", "IMPROVE_RUN_NOT_FOUND");
}
const { usageReport, strategy, note } = usageReportForRow(row, eventsCtx);
return {
mode: "run",
runId: row.id,
...(strategy !== undefined ? { strategy } : {}),
usageReport,
...(note ? { notes: [note] } : {}),
};
});
}