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
104 lines (103 loc) • 4.53 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/.
import { assertNever } from "./assert.js";
/**
* Map an {@link ImproveActionMode} to its coarse audit bucket. Single source of
* truth shared by `state-db.ts#computeImproveRunMetrics` and
* `improve.ts#emitImproveCompletedEvent` so the aggregate accepted/rejected/
* error counts can never disagree between the persisted `metrics_json` and the
* emitted `improve_completed` event.
*
* Buckets:
* - `accepted` — a write/content-authoring action succeeded.
* - `skipped` — the ref was GATED OUT before any content was produced: a
* cooldown, a signal-delta/eligibility skip, or a distill pool-delta skip.
* These are NOT rejections of produced content — they are the run declining
* to act on a ref it had no new reason to touch, and they scale with the
* whole indexed-ref pool (~13k/run), so folding them into `rejected` made the
* "accept rate" meaningless (deep-tuning analysis 2026-06-29, finding #1).
* - `rejected` — the run PRODUCED a change and a content-policy guard then
* rejected it (`reflect-guard-rejected`). This is the genuine value-rejection
* signal; it is small and meaningful, no longer drowned by gated skips.
* - `error` — the action failed (LLM/runtime error).
* - `noop` — bookkeeping that is neither a write nor a rejection (memory-prune);
* intentionally counted in none of the numeric buckets.
*
* The `default: assertNever(mode)` arm makes any future union variant a
* compile-time error here, forcing an explicit bucket choice.
*/
export function classifyImproveAction(mode) {
switch (mode) {
case "reflect":
case "distill":
case "memory-inference":
return "accepted";
case "reflect-cooldown":
case "reflect-skipped":
case "distill-skipped":
return "skipped";
case "reflect-guard-rejected":
return "rejected";
case "reflect-failed":
case "error":
return "error";
case "memory-prune":
return "noop";
default:
return assertNever(mode);
}
}
/**
* #912 — skip reasons that mean infrastructure failed to run the extraction,
* not that a session was legitimately uninteresting. `already_extracted`,
* `too_short`, and `triaged_out` are the ledger and pre-filter doing their
* job and are deliberately excluded; `malformed_model_output` already gets
* its own per-session forwarding into the envelope's `warnings[]` and is
* excluded here to avoid double-reporting the same failure two ways.
*
* Typed against `ExtractedSessionResult["skipReason"]` so a future reason
* cannot be added to one union without a compiler error surfacing here.
*/
export const EXTRACT_INFRASTRUCTURE_SKIP_REASONS = [
"llm_unavailable",
"read_failed",
"exception",
"locked_concurrent",
];
/** Upper bound on retained sample refs PER reason in {@link DistillSkippedAggregate}. */
export const DISTILL_SKIPPED_SAMPLE_CAP_PER_REASON = 3;
/**
* Partition an action list into the rows to persist and the `distill-skipped`
* aggregate. Pure — no I/O. Called once at improve-result assembly time so the
* serialized envelope never carries per-ref distill-skipped rows.
*
* Non-`distill-skipped` actions are returned verbatim and in order. When there
* are zero distill-skipped actions the aggregate is omitted (the envelope stays
* byte-identical to a run that skipped nothing).
*/
export function foldDistillSkipped(actions) {
const kept = [];
const byReason = {};
const samples = [];
const sampleCountByReason = {};
let total = 0;
for (const action of actions) {
if (action.mode !== "distill-skipped") {
kept.push(action);
continue;
}
total += 1;
const r = action.result;
const reason = typeof r?.reason === "string" && r.reason.trim() ? r.reason : "unknown";
byReason[reason] = (byReason[reason] ?? 0) + 1;
const seen = sampleCountByReason[reason] ?? 0;
if (seen < DISTILL_SKIPPED_SAMPLE_CAP_PER_REASON) {
sampleCountByReason[reason] = seen + 1;
samples.push({ ref: action.ref, reason });
}
}
if (total === 0)
return { actions: kept };
return { actions: kept, aggregate: { total, byReason, samples } };
}