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

104 lines (103 loc) • 4.53 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/. 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 } }; }