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

984 lines (983 loc) • 45.2 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/. /** * `akm distill <ref>` — distil an asset and its feedback into a lesson (or, * for a reinforced memory, a knowledge) proposal. One bounded LLM call, then * the shared judge → mint path in `./stage`; the proposal queue is the only * way to a live asset. Every invocation emits one `distill_invoked` event * carrying its `outcome` (config-disabled runs emit none). * * Lesson refs: a nested input keeps its first scope segment * (`memories/project-a/deploy` → `lessons/project-a/memory-deploy-lesson`); an * unscoped input stays flat. */ import fs from "node:fs"; import path from "node:path"; import distillKnowledgeSystemPrompt from "../../assets/prompts/distill-knowledge-system.md" with { type: "text" }; import distillLessonSystemPrompt from "../../assets/prompts/distill-lesson-system.md" with { type: "text" }; import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js"; import { assembleAsset, assembleAssetFromString, serializeFrontmatterQuoted } from "../../core/asset/asset-serialize.js"; import { parseFrontmatter, writeSalienceToFrontmatter } from "../../core/asset/frontmatter.js"; import { stripMarkdownFences } from "../../core/asset/markdown.js"; import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js"; import { authoringRulesForType } from "../../core/authoring-rules.js"; import { isWithin, resolveStashDir } from "../../core/common.js"; import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js"; import { UsageError } from "../../core/errors.js"; import { appendEvent, readEvents } from "../../core/events.js"; import { lintLessonContent } from "../../core/lesson-lint.js"; import { parseEmbeddedJsonResponse } from "../../core/parse.js"; import { getDbPath } from "../../core/paths.js"; import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js"; import { withStateDb } from "../../core/state-db.js"; import { warn, warnVerbose } from "../../core/warn.js"; import { recordWrittenPath } from "../../core/write-provenance.js"; import { resolveAssetPath } from "../../indexer/walk/path-resolver.js"; import { assertRunnerCredentials } from "../../integrations/agent/runner-dispatch.js"; import { closeDatabase, openReadonlyExistingDatabase } from "../../storage/repositories/index-connection.js"; import { getAllEntries } from "../../storage/repositories/index-entries-repository.js"; import { listProposals } from "../proposal/repository.js"; import { detectDoubleFrontmatter, isValidDescription } from "../proposal/validators/proposal-quality-validators.js"; import { akmSearch } from "../read/search.js"; import { stripFrontmatterBody } from "./content-hash.js"; import { autoRepairLessonFrontmatter, autoSwapDescriptionWhenToUse, collectLessonQualityFindings, repairLessonDescriptionTruncation, } from "./distill/content-repair.js"; import { buildClsContext, checkDistillFidelity, DEFAULT_CLS_ADJACENT_COUNT } from "./distill-guards.js"; import { assessMemoryKnowledgePromotionCandidate, deriveKnowledgeRef } from "./distill-promotion-policy.js"; import { buildRefVocabulary, scoreEncodingSalience } from "./encoding-salience.js"; import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js"; import { recordLedgerAttempt } from "./ledger.js"; import { computeSalience, upsertAssetSalience } from "./salience.js"; import { callStage, mintProposal, noticeSet, rejectedProposalContext, resolveQualityGateJudge, runLessonQualityJudge, stageRunner, } from "./stage.js"; /** * Input types distill structurally refuses: a lesson is the distilled form * (distilling one would mint `lessons/lesson-…-lesson`), and env/secret bytes * must never reach the model. The improve planner skips these before queuing. */ export const DISTILL_REFUSED_INPUT_TYPES = new Set(["lesson", "env", "secret"]); export function isDistillRefusedInputType(type) { return DISTILL_REFUSED_INPUT_TYPES.has(type); } /** Derive the proposed lesson ref from the input ref. */ export function deriveLessonRef(inputRef) { const parsed = parseRefInput(inputRef); const parts = parsed.name.split("/"); const scope = parts.length > 1 ? parts.shift() : undefined; const clean = (value) => value .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); const safeScope = scope ? clean(scope) : ""; return `lessons/${safeScope ? `${safeScope}/` : ""}${clean(`${parsed.type}-${parts.join("-")}`)}-lesson`; } // ── Output contract ────────────────────────────────────────────────────────── // // The client sends a response schema `strict: true`, and a strict provider // (OpenAI's) rejects an object whose `required` leaves out any of its // properties, so every property is required and "none" is an empty array (#1046). export const DISTILL_LESSON_JSON_SCHEMA = { type: "object", required: ["reason", "decision", "description", "when_to_use", "body", "tags"], additionalProperties: false, properties: { reason: { type: "string", description: "One sentence, written first: the cause and the fix the memory states, or why it states none.", }, decision: { type: "string", enum: ["lesson", "none"], description: "`none` when the memory holds no lesson (it records what was done, a design, or steps already written elsewhere): leave the other fields empty. Otherwise `lesson`.", }, description: { type: "string", description: "Single complete sentence summarising what the lesson teaches. No markdown, no leading 'When'/'If'. Empty for `none`.", }, when_to_use: { type: "string", description: "Single complete sentence describing the concrete trigger condition for the lesson. Empty for `none`.", }, body: { type: "string", description: "Lesson body: plain markdown, shorter than the memory, stating only what it and its feedback say. Empty for `none`.", }, tags: { type: "array", items: { type: "string" }, description: "Tag list. Use an empty array for none; the post-processor drops it if empty.", }, }, }; export const DISTILL_KNOWLEDGE_JSON_SCHEMA = { type: "object", required: ["description", "body", "tags", "sources"], additionalProperties: false, properties: { description: { type: "string", minLength: 1, description: "One-line summary of the knowledge asset." }, body: { type: "string", minLength: 1, description: "Knowledge body — structured markdown with a `# Title` heading and durable facts only.", }, tags: { type: "array", items: { type: "string" }, description: "Tag list. Use an empty array for none; the post-processor drops it if empty.", }, sources: { type: "array", items: { type: "string" }, description: "Source refs the knowledge was distilled from. Use an empty array for none.", }, }, }; /** * Assemble markdown from a structured-output payload, or `null` when a * required field is empty (the caller then treats the response as markdown). */ export function assembleStructuredDistillMarkdown(payload, kind) { if (payload === null || typeof payload !== "object") return null; const text = (value) => (typeof value === "string" ? value.trim() : ""); const list = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim().length > 0) : []; const description = text(payload.description); const body = text(payload.body); if (!description || !body) return null; const fm = { description }; if (kind === "lesson") { const whenToUse = text(payload.when_to_use); if (!whenToUse) return null; fm.when_to_use = whenToUse; } const tags = list(payload.tags); if (tags.length > 0) fm.tags = tags; const sources = kind === "knowledge" ? list(payload.sources) : []; if (sources.length > 0) fm.xrefs = sources; return assembleAssetFromString(serializeFrontmatterQuoted(fm), body); } /** * The writer's answer when it found no lesson: the word NONE, or `decision: "none"` in a reply bound to the schema, * with the reason it gave (`""` for the bare word). `null` for any other reply. */ function answeredNone(raw) { if (/^[\s"'`*_]*none[\s.!"'`*_]*$/i.test(stripMarkdownFences(raw))) return { reason: "" }; const payload = parseEmbeddedJsonResponse(raw); if (payload === null || typeof payload !== "object" || Array.isArray(payload) || payload.decision !== "none") { return null; } return { reason: typeof payload.reason === "string" ? payload.reason.trim() : "" }; } function validateKnowledgeContent(content, inputRef) { const findings = []; const parsed = parseFrontmatter(content); if (parsed.content.trim().length === 0) { findings.push({ kind: "missing-body", field: "body", message: `Distilled knowledge for ${inputRef} must include a non-empty markdown body.`, }); } // A present description must be a real summary (not `---` or a heading fragment). const description = parsed.data?.description; if (description !== undefined) { const check = isValidDescription(description, inputRef, { skipRefTailCheck: true }); if (!check.ok) { findings.push({ kind: "invalid-description", field: "description", message: `Distilled knowledge for ${inputRef} has an invalid description: ${check.reason}.`, }); } } const doubled = detectDoubleFrontmatter(content); if (doubled) { findings.push({ kind: doubled.kind, field: "body", message: `Distilled knowledge for ${inputRef}: ${doubled.message}`, }); } return findings; } /** * The distill user prompt. Feedback is rendered as "What worked" / "What * failed" contrast when it carries signals, else as a flat event list. */ export function buildDistillPrompt(input) { const lines = [`Asset ref: ${input.inputRef}`, ""]; if (input.standardsContext?.trim()) { lines.push("Standards to follow (the rulebook for this target):", input.standardsContext.trim(), ""); } const authoringRules = authoringRulesForType(input.proposalKind ?? "lesson"); if (authoringRules) lines.push(authoringRules, ""); lines.push("Asset content:"); if (input.assetContent) { // Source frontmatter is not evidence; fed verbatim, models copied it into the body. lines.push("```", parseFrontmatter(input.assetContent).content.trim().slice(0, 3000), "```"); } else { lines.push("(asset is not currently indexed; distil from feedback signal alone)"); } lines.push(""); const flat = (event) => `- ${event.ts} ${event.eventType}${event.metadata ? ` ${JSON.stringify(event.metadata)}` : ""}`; if (input.feedback.length === 0) { lines.push("Recent feedback: (no feedback events recorded — distil from the asset itself)"); } else { const worked = []; const failed = []; const other = []; for (const event of input.feedback) { const meta = event.metadata ?? {}; const detail = (typeof meta.reason === "string" ? meta.reason : "") || (typeof meta.note === "string" ? meta.note : ""); const line = `- ${event.ts}: ${detail || "feedback received"}`; if (meta.signal === "positive") worked.push(line); else if (meta.signal === "negative") failed.push(line); else other.push(flat(event)); } if (worked.length > 0 || failed.length > 0) { for (const [heading, section] of [ ["## What worked", worked], ["## What failed", failed], ["## Other signals", other], ]) { if (section.length > 0) lines.push(heading, ...section, ""); } } else { lines.push("Recent feedback events (most recent last):", ...input.feedback.map(flat), ""); } } if (input.rejectedProposals && input.rejectedProposals.length > 0) { lines.push("", "Previously rejected proposals for this ref (Reflexion context):", "The following proposals were already reviewed and rejected. " + "Your new proposal MUST differ meaningfully in approach, framing, or evidence."); for (const rp of input.rejectedProposals) { lines.push(`- Rejection reason: ${rp.reason}`); if (rp.contentPreview) lines.push(` Content preview: ${rp.contentPreview.slice(0, 200).replace(/\n/g, " ")}`); } } lines.push(input.proposalKind === "knowledge" ? "Produce the knowledge markdown file now. Start your response with `---` on the first line, followed by a `description:` field whose value is a 1-sentence summary (20–400 chars). Never use placeholder values like `---`, `tbd`, `n/a`, or a single dash. If the source has nothing meaningful to summarize, do NOT produce a proposal — return an empty response instead. The frontmatter block ends with a second `---` line; do not emit any additional `---` fences in the body." : "Produce the lesson markdown file now. Start your response with `---` on the first line, followed by `description:` and `when_to_use:` fields. Both must be real one-sentence summaries (20–400 chars) — never placeholder values like `---`, `tbd`, or `n/a`. The frontmatter block ends with a second `---` line; do not emit any additional `---` fences in the body. If the memory holds no lesson, answer NONE instead."); return lines.join("\n"); } // ── Invocation ─────────────────────────────────────────────────────────────── const DISABLED_MESSAGE = "distill is disabled in config; enable processes.distill.enabled to activate."; function emitDistill(run, meta) { appendEvent({ eventType: "distill_invoked", ref: run.ledgerRef, metadata: { ...meta, ...run.eligMeta } }, run.options.eventsCtx); } /** * End a distill with no proposal and no failure: reported as `skipped`, which the improve loop * leaves in the ledger as unchanged, as it does a reflect that changed nothing. */ function skipDistill(run, proposalRef, kind, skipReason, message) { emitDistill(run, { outcome: "skipped", proposalRef, proposalKind: kind, skipReason, message, ...exclusionMeta(run, false), }); return { schemaVersion: 1, ok: true, outcome: "skipped", inputRef: run.inputRef, proposalRef, proposalKind: kind, skipReason, message, ...exclusionMeta(run, true), }; } /** The exclusion diagnostics for an event (count only) or a result (count + fully-filtered). */ function exclusionMeta(run, forResult) { if (!run.exclusion) return {}; return forResult ? { ...run.exclusion } : { filteredFeedbackCount: run.exclusion.filteredFeedbackCount }; } export async function akmDistill(options) { const inputRef = options.ref.trim(); if (!inputRef) { throw new UsageError("Asset ref is required. Usage: akm distill <ref>", "MISSING_REQUIRED_ARGUMENT"); } const parsedInputRef = parseRefInput(inputRef); const config = options.config ?? loadConfig(); const profile = options.improveProfile ?? resolveImproveStrategy(undefined, config).config; options = { ...options, improveProfile: profile }; const targetKind = options.proposalKind ?? "lesson"; const kind = targetKind === "knowledge" ? "knowledge" : "lesson"; const outputRef = kind === "knowledge" ? deriveKnowledgeRef(inputRef) : deriveLessonRef(inputRef); if (!resolveProcessEnabled("distill", profile)) { return { schemaVersion: 1, ok: true, outcome: "config_disabled", inputRef, proposalRef: outputRef, proposalKind: kind, message: DISABLED_MESSAGE, }; } const ledgerRef = options.itemRef ?? inputRef; const eligMeta = options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}; if (isDistillRefusedInputType(parsedInputRef.type)) { const secret = parsedInputRef.type === "env" || parsedInputRef.type === "secret"; const proposalRef = secret ? inputRef : conceptIdFromTypeName("lesson", parsedInputRef.name); const skipReason = secret ? "refused_secret_input" : "recursive_lesson_input"; const message = secret ? `Distill refuses ${parsedInputRef.type} inputs — secret material must never be sent to the LLM.` : "Distill refuses lesson inputs — lessons are the distilled form, not a source."; emitDistill({ ledgerRef, eligMeta, options }, { outcome: "skipped", proposalRef, message, skipReason }); return { schemaVersion: 1, ok: true, outcome: "skipped", inputRef, proposalRef, skipReason, message }; } const stash = options.stashDir ?? resolveStashDir(); const notices = noticeSet(); const lookup = options.lookupFn ?? ((ref) => defaultLookup(ref, stash)); const asset = await loadInput(lookup, inputRef); const run = { options, inputRef, ledgerRef, stash, config, profile, runner: stageRunner(options, config, profile, "distill", notices.add), judgeRunner: resolveQualityGateJudge(config, profile, "distill", notices.add), notices, eligMeta, asset, vocabulary: loadRefVocabulary(), outcomeWeightEnabled: config.improve?.salience?.outcomeWeightEnabled !== false, related: options.fetchRelatedFn ?? fetchRelatedAssets, lookup, }; const feedbackEvents = readDistillFeedback(run); const result = await distill(run, targetKind, kind, outputRef, feedbackEvents); return { ...result, ...notices.fields() }; } async function distill(run, targetKind, kind, outputRef, feedbackEvents) { // A reinforced memory graduates to knowledge without a generation call. const promotion = targetKind === "lesson" ? null : await planPromotion(run, feedbackEvents); if (promotion) { if (run.runner && promotion.existing) assertRunnerCredentials(run.runner); const judge = run.judgeRunner ?? run.runner; if (judge && qualityGateEnabled(run)) assertRunnerCredentials(judge); const promoted = await promoteToKnowledge(run, promotion); stampInputSalience(run); return promoted; } // A lesson already at the target ref is left as it is: the proposal would overwrite it, and all 5 recorded // overwrites were rejected. Checked before the call, which it would waste. if (kind === "lesson" && lessonExists(run, outputRef)) { return skipDistill(run, outputRef, kind, "lesson_exists", `${outputRef} already exists; distill does not overwrite a lesson.`); } const feedback = feedbackEvents.slice(-20).map((event) => ({ ts: event.ts, eventType: event.eventType, ...(event.metadata !== undefined ? { metadata: event.metadata } : {}), })); const { system, prompt } = await buildDistillMessages(run, feedback, kind, outputRef); const call = run.runner ? await callStage({ feature: "distill", runner: run.runner, system, prompt, gate: { config: run.config, enabled: true }, // NONE is an answer: a parser that rejected it would ask for a lesson again. parse: (raw) => (answeredNone(raw) ? raw : parseEmbeddedJsonResponse(raw)), // The injected test transport never sees the schema. request: { ...(run.options.chat === undefined ? { responseSchema: kind === "knowledge" ? DISTILL_KNOWLEDGE_JSON_SCHEMA : DISTILL_LESSON_JSON_SCHEMA } : { chat: run.options.chat }), ...(run.options.signal ? { signal: run.options.signal } : {}), }, onNotices: run.notices.add, }) : { ok: false, reason: "error" }; // Durable input salience waits until the credential-bearing dispatch returned. stampInputSalience(run); if (!call.ok || call.raw.trim() === "") { if (!call.ok) warnVerbose(`[akm] LLM fallback for distill: ${call.reason}`); emitDistill(run, { outcome: "llm_failed", proposalRef: outputRef, proposalKind: kind, ...exclusionMeta(run, false), }); return { schemaVersion: 1, ok: true, outcome: "llm_failed", inputRef: run.inputRef, proposalRef: outputRef, proposalKind: kind, message: "LLM call returned no usable output (timeout, empty, or error).", ...exclusionMeta(run, true), }; } const none = answeredNone(call.raw); if (none) { return skipDistill(run, outputRef, kind, "nothing_reusable", `The writer found no lesson in ${run.inputRef}${none.reason ? `: ${none.reason}` : "."}`); } const assembled = assembleDistilledContent(run, call.raw, kind, outputRef); if ("rejection" in assembled) return assembled.rejection; return judgeAndQueue(run, { ref: outputRef, kind, content: assembled.content, source: run.asset.content, descriptionSwapped: assembled.descriptionSwapped, feedback: feedbackLines(feedback), }); } /** The feedback that says something, one line each, for the judge. A bare signal says nothing the writer could use. */ function feedbackLines(feedback) { const lines = []; for (const event of feedback) { const meta = event.metadata ?? {}; const detail = (typeof meta.reason === "string" ? meta.reason : "") || (typeof meta.note === "string" ? meta.note : ""); if (detail.trim()) lines.push(`- [${typeof meta.signal === "string" ? meta.signal : event.eventType}] ${detail.trim()}`); } return lines; } /** Whether a file already holds the lesson `ref` in the stash the proposal would be filed in. */ function lessonExists(run, ref) { const { type, name } = parseRefInput(ref); const typeDir = stashDirFor(type); if (!typeDir) return false; try { return fs.statSync(assetPathForName(type, path.join(run.stash, typeDir), name)).isFile(); } catch { return false; } } /** Turn the response into validated content: structured JSON or markdown, then lesson repairs and lint. */ function assembleDistilledContent(run, raw, kind, outputRef) { const structured = parseEmbeddedJsonResponse(raw); let content = (structured && !Array.isArray(structured) ? assembleStructuredDistillMarkdown(structured, kind) : null) ?? stripMarkdownFences(raw); let descriptionSwapped = 0; if (kind === "lesson") { content = autoRepairLessonFrontmatter(content, run.inputRef); ({ content, swapped: descriptionSwapped } = autoSwapDescriptionWhenToUse(content, run.inputRef)); content = repairLessonDescriptionTruncation(content); } // Required structure missing means there is no asset to write: a hard reject. const structural = kind === "knowledge" ? validateKnowledgeContent(content, run.inputRef) : lintLessonContent(content, `distill:${run.inputRef}`).findings; if (structural.length > 0) { emitDistill(run, { outcome: "validation_failed", proposalRef: outputRef, proposalKind: kind, findingKinds: structural.map((f) => f.kind), ...exclusionMeta(run, false), }); throw new UsageError(`Distilled ${kind} failed validation:\n${structural.map((f) => f.message).join("\n")}`, "MISSING_REQUIRED_ARGUMENT", kind === "knowledge" ? "Knowledge proposals require a non-empty markdown body." : "Lessons require non-empty `description` and `when_to_use` frontmatter fields. See v1 spec §13."); } // Heuristic quality findings go to a human, not the bin. const quality = kind === "lesson" ? collectLessonQualityFindings(content, run.inputRef) : []; if (quality.length > 0) { return { rejection: rejectDistilled(run, outputRef, content, 2.0, quality.map((f) => f.message).join("\n"), { reviewNeeded: true, proposalKind: kind, findingKinds: quality.map((f) => f.kind), }), }; } return { content, descriptionSwapped }; } function qualityGateEnabled(run) { return run.profile.processes?.distill?.qualityGate?.enabled ?? true; } /** * Judge the distilled content, then queue it for review. A rejected, uncertain or * source-contradicting result is recorded instead (see {@link writeQualityRejection}). */ async function judgeAndQueue(run, out) { let content = out.content; let confidence; let judged; if (qualityGateEnabled(run)) { const related = await run.related(content.slice(0, 500), RELATED_COUNT); // The judge reads what the generator read: the source body, without its frontmatter (buildDistillPrompt). const source = out.source ? parseFrontmatter(out.source).content.trim() : ""; const verdict = await runLessonQualityJudge(run.config, content, source, run.options.chat, { ...(related.length > 0 ? { related } : {}), ...(out.feedback && out.feedback.length > 0 ? { feedback: out.feedback } : {}), ...((run.judgeRunner ?? run.runner) ? { llmRunner: run.judgeRunner ?? run.runner } : {}), ...(run.options.signal ? { signal: run.options.signal } : {}), onNotices: run.notices.add, }); if (!verdict.pass) { return rejectDistilled(run, out.ref, content, verdict.score, verdict.reason, { ...(verdict.reviewNeeded ? { reviewNeeded: true } : {}), ...(verdict.criteria ? { criteria: verdict.criteria } : {}), }); } if (verdict.score > 0) confidence = verdict.score / 5; judged = verdict; } let frontmatter; if (out.promotion) { const data = parseFrontmatter(content).data; if (Object.keys(data).length > 0) frontmatter = data; } else { // Optional check against the cited source; a contradiction goes to a human. const fidelity = getImproveProcessConfig("distill", run.profile)?.fidelityCheck ?? {}; if (fidelity.enabled && out.source) { try { const verdict = checkDistillFidelity(stripFrontmatterBody(content), [stripFrontmatterBody(out.source)], fidelity); if (verdict.contradictionDetected) { return rejectDistilled(run, out.ref, content, 2.0, verdict.reason ?? "Proposal may contradict cited source memories.", { reviewNeeded: true, fidelityContradiction: true }); } } catch { // The fidelity check is supplemental. } } // Canonical provenance goes into the content promotion writes. const parsed = parseFrontmatter(content); const xrefs = Array.isArray(parsed.data.xrefs) ? parsed.data.xrefs.map(String) : []; frontmatter = { ...parsed.data, xrefs: [...new Set([...xrefs, run.inputRef])] }; delete frontmatter.sources; content = assembleAsset(frontmatter, parsed.content); } const proposal = mintProposal(run.stash, run.options.ctx, { ref: out.ref, source: "distill", ...(run.options.sourceRun !== undefined ? { sourceRun: run.options.sourceRun } : {}), payload: { content, ...(frontmatter ? { frontmatter } : {}) }, ...(confidence !== undefined ? { confidence } : {}), ...(run.options.eligibilitySource ? { eligibilitySource: run.options.eligibilitySource } : {}), // The ledger keys the attempt by the input, not the output. attemptedRefs: [run.ledgerRef], }, // A pass goes to a person, never to the triage drain or its judgment tier. Staged precision was 2 of 12 on // 2026-10-05: ten of the staged lessons restated their memory, claimed what it does not say, or filed a dated // status as a lesson, and no judge score separated them from the two good ones. The judge's evidence stays on // the decision for the reviewer. With the gate off nothing was judged, and the drain decides as before. judged ? { review: { reason: "distill-review", gate: "quality-gate", ...(judged.criteria ? { scores: judged.criteria } : {}), judgeReason: judged.reason, }, } : {}); persistOutputEncodingSalience(run, out.ref, content); const swapped = out.descriptionSwapped ? { descriptionSwapped: out.descriptionSwapped } : {}; emitDistill(run, { outcome: "queued", proposalRef: out.ref, proposalKind: out.kind, proposalId: proposal.id, ...(confidence !== undefined ? { judgeConfidence: confidence } : {}), ...(run.options.sourceRun !== undefined ? { sourceRun: run.options.sourceRun } : {}), ...exclusionMeta(run, false), ...swapped, }); return { schemaVersion: 1, ok: true, outcome: "queued", inputRef: run.inputRef, proposalRef: out.ref, proposalKind: out.kind, proposalId: proposal.id, proposal, ...exclusionMeta(run, true), ...swapped, }; } function rejectDistilled(run, proposalRef, content, score, reason, meta) { return writeQualityRejection({ stash: run.stash, inputRef: run.inputRef, proposalRef, content, score, reason, meta: { ...meta, ...exclusionMeta(run, true) }, eligibilitySource: run.options.eligibilitySource, eventsCtx: run.options.eventsCtx, proposalsCtx: run.options.ctx, sourceRun: run.options.sourceRun, ledgerRef: run.ledgerRef, }); } /** Longest lesson text kept on a rejection, the same cap the quality judge's prompt reads (stage.ts). */ const REJECTED_CONTENT_MAX_CHARS = 2000; /** * Record a distill quality-gate outcome and return its envelope. * `quality_rejected` lands in the improve ledger under the input's key (its * rejection window keeps selection from regenerating it); `review_needed` * mints a pending proposal for a human, stamped `deferred`/`quality-gate` so * the triage drain leaves it alone. Content the mint refuses still records * the attempt. */ export function writeQualityRejection(args) { const meta = args.meta ?? {}; const outcome = meta.reviewNeeded ? "review_needed" : "quality_rejected"; const ledgerRef = args.ledgerRef ?? args.inputRef; const access = { proposalsCtx: args.proposalsCtx, eventsCtx: args.eventsCtx }; const attempt = { stashDir: args.stash, ref: ledgerRef, source: "distill", detail: args.reason }; let proposal; if (outcome === "quality_rejected") { recordLedgerAttempt(access, { ...attempt, outcome: "quality_rejected" }); } else { try { proposal = mintProposal(args.stash, args.proposalsCtx, { ref: args.proposalRef, source: "distill", ...(args.sourceRun !== undefined ? { sourceRun: args.sourceRun } : {}), payload: { content: args.content }, attemptedRefs: [ledgerRef], ...(args.eligibilitySource ? { eligibilitySource: args.eligibilitySource } : {}), }, { review: { reason: "quality-review", gate: "quality-gate" } }); } catch (error) { warn(`[akm] writeQualityRejection: failed to queue ${args.proposalRef} for review: ${error instanceof Error ? error.message : String(error)}`); recordLedgerAttempt(access, { ...attempt, outcome: "review_needed" }); } } const eligMeta = args.eligibilitySource ? { eligibilitySource: args.eligibilitySource } : {}; // The text the gate turned away, kept in the local event and the result so a rejection can be audited. const rejectedContent = args.content.slice(0, REJECTED_CONTENT_MAX_CHARS); appendEvent({ eventType: "distill_invoked", ref: ledgerRef, metadata: { outcome, proposalRef: args.proposalRef, score: args.score, reason: args.reason, rejectedContent, ...meta, ...eligMeta, }, }, args.eventsCtx); return { schemaVersion: 1, ok: true, outcome, inputRef: args.inputRef, proposalRef: args.proposalRef, score: args.score, reason: args.reason, rejectedContent, ...(proposal ? { proposalId: proposal.id, proposal } : {}), ...meta, }; } async function planPromotion(run, feedbackEvents) { const assessment = assessMemoryKnowledgePromotionCandidate({ inputRef: run.inputRef, assetContent: run.asset.content, feedbackEvents, }); if (!assessment.promote || !assessment.content) return null; const existingPath = await run.lookup(assessment.knowledgeRef); let existing = null; try { // The promotion is filed in run.stash, so only that bundle's doc is its destination (#1000). if (existingPath && fs.existsSync(existingPath) && isWithin(existingPath, run.stash)) { existing = fs.readFileSync(existingPath, "utf8"); } } catch { existing = null; } return { knowledgeRef: assessment.knowledgeRef, content: assessment.content, existing }; } /** * Promote a reinforced memory to knowledge. An existing destination is * reconciled by the model (ADD/UPDATE swap content in, NOOP keeps what is * there); without a model the existing content is appended for the reviewer. */ async function promoteToKnowledge(run, plan) { let content = plan.content; if (plan.existing && run.runner) { const merged = await callStage({ feature: "distill", runner: run.runner, system: "Return only valid JSON. No prose.", prompt: [ "You are merging two versions of a knowledge document.", "Existing content is already committed; new content comes from a memory distillation run.", "Choose one of: ADD (combine both), UPDATE (replace existing with new), NOOP (keep existing unchanged).", 'Return ONLY valid JSON: {"action": "ADD"|"UPDATE"|"NOOP", "content": "<merged markdown if ADD/UPDATE, empty string if NOOP>"}', "", "## Existing knowledge content", "```", plan.existing.slice(0, 3000), "```", "", "## New content from distillation", "```", plan.content.slice(0, 3000), "```", ].join("\n"), request: { ...(run.options.signal ? { signal: run.options.signal } : {}), ...(run.options.chat ? { chat: run.options.chat } : {}), }, onNotices: run.notices.add, }); const decision = merged.ok ? parseEmbeddedJsonResponse(merged.raw) : undefined; if (decision?.action === "NOOP") { emitDistill(run, { outcome: "skipped", proposalRef: plan.knowledgeRef, message: "D-1: LLM resolved destination conflict as NOOP — existing content kept", }); return { schemaVersion: 1, ok: true, outcome: "skipped", inputRef: run.inputRef, proposalRef: plan.knowledgeRef, skipReason: "conflict_noop", message: "Existing knowledge content unchanged (contradiction resolution: NOOP)", }; } if ((decision?.action === "ADD" || decision?.action === "UPDATE") && decision.content?.trim()) { content = decision.content; } } else if (plan.existing) { content = [ plan.content, "", "---", "<!-- D-1 / #369: Existing knowledge content is shown below for reviewer reference. -->", "<!-- Review: decide whether to ADD (merge), UPDATE (replace), or NOOP (keep existing). -->", "", "## Existing content (for reviewer reference)", "", plan.existing, ].join("\n"); } return judgeAndQueue(run, { ref: plan.knowledgeRef, kind: "knowledge", content, source: run.asset.content, promotion: true, }); } // ── Inputs ─────────────────────────────────────────────────────────────────── /** Read the input asset (best-effort: an unindexed asset distils from feedback alone). */ async function loadInput(lookup, inputRef) { try { const filePath = await lookup(inputRef); if (filePath && fs.existsSync(filePath)) return { path: filePath, content: fs.readFileSync(filePath, "utf8") }; } catch { // An index miss is not fatal. } return { path: null, content: null }; } /** The index's ref bigram vocabulary, for the novelty term of encoding salience. */ function loadRefVocabulary() { try { const db = openReadonlyExistingDatabase(getDbPath(), { isolatedSnapshot: true }); if (!db) return new Set(); try { return buildRefVocabulary(getAllEntries(db).map((e) => e.itemRef)); } finally { closeDatabase(db); } } catch { return new Set(); } } /** * Score the input's encoding salience and mirror it to the asset frontmatter * and `asset_salience` (keyed by the ledger ref). Best-effort throughout. */ function stampInputSalience(run) { const { content, path: filePath } = run.asset; if (!content || !filePath) return; try { const type = parseRefInput(run.inputRef).type; let revisionCount = 0; try { // Revisions so far: every proposal raised against this ref. revisionCount = listProposals(run.stash, { ref: run.inputRef, includeArchive: true }).length; } catch { // Unknown history scores as a first encounter. } const scored = scoreEncodingSalience({ body: content, type, existingRefVocabulary: run.vocabulary, revisionCount }); const updated = writeSalienceToFrontmatter(content, scored.score, scored); if (updated !== content) { fs.writeFileSync(filePath, updated, "utf8"); recordWrittenPath(filePath); run.asset.content = updated; } try { withStateDb((stateDb) => upsertAssetSalience(stateDb, run.ledgerRef, computeSalience({ ref: run.inputRef, type, retrievalFreq: 0, encodingSalience: scored.score, outcomeWeightEnabled: run.outcomeWeightEnabled, }))); } catch { // The frontmatter mirror is the only persistence then. } } catch { // Scoring never blocks distillation. } } /** * Content-score a distilled output so it carries a real encoding salience from * creation — lessons are refused as inputs, so this is their only chance. */ function persistOutputEncodingSalience(run, ref, body) { try { const type = parseRefInput(ref).type; const scored = scoreEncodingSalience({ body, type, existingRefVocabulary: run.vocabulary, revisionCount: 0 }); withStateDb((stateDb) => upsertAssetSalience(stateDb, ref, computeSalience({ ref, type, retrievalFreq: 0, encodingSalience: scored.score, outcomeWeightEnabled: run.outcomeWeightEnabled, }))); } catch { // Scoring never blocks proposal creation. } } /** The ref's feedback events, minus any `excludeFeedbackFromRefs` matches. */ function readDistillFeedback(run) { const read = run.options.readEventsFn ?? ((readOptions) => readEvents(readOptions, { readOnly: true })); const { events } = read({ ref: run.ledgerRef, type: "feedback", excludeTags: run.options.excludeTags, includeTags: run.options.includeTags, }); const excluded = new Set((run.options.excludeFeedbackFromRefs ?? []).map((ref) => ref.trim()).filter((ref) => ref.length > 0)); if (excluded.size === 0) return events; const kept = events.filter((e) => !(e.ref !== undefined && excluded.has(e.ref))); run.exclusion = { filteredFeedbackCount: events.length - kept.length, feedbackFullyFiltered: events.length > 0 && kept.length === 0, }; return kept; } /** System + user prompt: rejected-proposal context, optional CLS neighbours, stash standards. */ async function buildDistillMessages(run, feedback, kind, outputRef) { const rejectedProposals = rejectedProposalContext(run.stash, run.inputRef, run.options.ctx, run.options.eventsCtx); // CLS interleaving (default on): show the related lessons, knowledge notes and skills, so the writer neither repeats nor overwrites them. const cls = getImproveProcessConfig("distill", run.profile)?.cls ?? {}; let clsContext = ""; if (cls.enabled !== false) { try { const query = run.asset.content ? run.asset.content.slice(0, 500) : run.inputRef; clsContext = buildClsContext(await run.related(query, cls.adjacentCount ?? DEFAULT_CLS_ADJACENT_COUNT), cls); } catch { // CLS context is supplemental. } } const standardsContext = resolveStandardsContext(outputRef, run.stash); const prompt = buildDistillPrompt({ inputRef: run.inputRef, assetContent: run.asset.content, feedback, proposalKind: kind, ...(rejectedProposals.length > 0 ? { rejectedProposals } : {}), ...(standardsContext.trim() ? { standardsContext } : {}), }); return { system: kind === "knowledge" ? distillKnowledgeSystemPrompt : distillLessonSystemPrompt, prompt: `${prompt}${clsContext}`, }; } async function defaultLookup(ref, stashDir) { return resolveAssetPath(ref, { stashDir, mode: "disk-only", directoryIndexNames: ["SKILL.md"], preserveDirectNameFallback: true, honorOrigin: false, }); } /** What the library already holds on a subject: lessons and knowledge notes say it, a skill is how to do it. */ const RELATED_TYPES = ["lesson", "knowledge", "skill"]; const RELATED_COUNT = 3; /** The top-N lessons, knowledge notes and skills related to `query`, best first (empty when search is unavailable). */ async function fetchRelatedAssets(query, n) { try { // One search per type: memories outnumber the rest and would fill an untyped list. const results = await Promise.all(RELATED_TYPES.map((type) => akmSearch({ query, type, limit: n, skipLogging: true, eventSource: "improve" }))); return results .flatMap((result) => result?.hits ?? []) .filter((h) => "path" in h && typeof h.path === "string") .sort((a, b) => (b.score ?? 0) - (a.score ?? 0)) .slice(0, n) .map((h) => { let content = ""; try { if (h.path && fs.existsSync(h.path)) content = fs.readFileSync(h.path, "utf8"); } catch { // best-effort } return { ref: h.ref, content }; }); } catch { return []; } }