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

1,007 lines (1,006 loc) • 48.3 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 reflect [ref]` — ask an engine whether an asset's `description`, * `when_to_use` and title need a fix, and queue the asset with that fix as a * proposal (`source: "reflect"`). The engine never writes the body: akm keeps * it byte for byte. Reflect never writes an asset: the proposal queue is the * only path, `akm proposal accept` the bridge. * * Every invocation closes with one `reflect_completed` event; `reflect_invoked` * is emitted once the dispatch has validated its credentials (deterministic * pre-dispatch refusals still emit both). */ import fs from "node:fs"; import { serializeFrontmatter } from "../../core/asset/asset-serialize.js"; import { parseFrontmatter } from "../../core/asset/frontmatter.js"; import { parseRefInput } from "../../core/asset/resolve-ref.js"; import { DESCRIPTION_MAX_CHARS, requiresDescription } from "../../core/authoring-rules.js"; import { resolveStashDir } from "../../core/common.js"; import { loadConfig } from "../../core/config/config.js"; import { generatedContentRejection } from "../../core/content-safety.js"; import { ConfigError, 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 { redactSensitiveText } from "../../core/redaction.js"; import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js"; import { warn, warnOnce } from "../../core/warn.js"; import { lookup } from "../../indexer/indexer.js"; import { DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js"; import { fallbackAnnouncement, NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, withEngineFallback, } from "../../integrations/agent/engine-fallback.js"; import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js"; import { buildReflectOutputRepairPrompt, buildReflectPrompt, REFLECT_CONTENT_CAP, } from "../../integrations/agent/prompts.js"; import { runnerIsLlm } from "../../integrations/agent/runner.js"; import { assertRunnerCredentials, collectDispatchSensitiveValues, } from "../../integrations/agent/runner-dispatch.js"; import { isJsonSchemaKnownUnsupported, LlmCallError } from "../../llm/client.js"; import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js"; import { isValidDescription } from "../proposal/validators/proposal-quality-validators.js"; import { CHARS_PER_TOKEN, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js"; import { findAssetFilePath } from "./eligibility.js"; import { resolveImproveExecution } from "./execution.js"; import { recordLedgerAttempt } from "./ledger.js"; import { classifyReflectChange, findReflectDefect, splitFrontmatter } from "./reflect-noise.js"; import { loadRetrievalQueries, runRetrievalRegressionGate } from "./retrieval-gate.js"; import { callStageOnce, errMessage, mintProposal, noticeSet, rejectedProposalContext, resolveQualityGateJudge, runReflectQualityJudge, } from "./stage.js"; const MAX_FEEDBACK_LINES = 10; const MAX_GLOBAL_FEEDBACK_LINES = 20; function readOnlyEventsContext(ctx) { return ctx?.db ? ctx : { ...(ctx ?? {}), readOnly: true }; } /** Recent `feedback` lines for `ref` (or across all assets without one). Best-effort. */ function readRecentFeedback(ref, eventsCtx) { try { const events = readEvents({ type: "feedback", ...(ref ? { ref } : {}) }, readOnlyEventsContext(eventsCtx)).events; return events.slice(-(ref ? MAX_FEEDBACK_LINES : MAX_GLOBAL_FEEDBACK_LINES)).map((event) => { const md = event.metadata ?? {}; const signal = typeof md.signal === "string" ? md.signal : "?"; const note = typeof md.reason === "string" ? md.reason : typeof md.note === "string" ? md.note : ""; const details = note ? `[${signal}] ${note}` : `[${signal}]`; return !ref && event.ref ? `${event.ref} ${details}` : details; }); } catch { return []; } } /** * Types reflect may patch: its output is frontmatter + markdown, which would * break a script or env file. Another type is allowed only when its current * content already has that shape; secrets are never read. */ export const REFLECT_ALLOWED_TYPES = new Set([ "knowledge", "memory", "lesson", "skill", "agent", "command", "workflow", ]); const REFLECT_REFUSED_TYPES = new Set(["secret"]); /** Lesson lint findings for the prompt: a concrete starting point for the revision. */ function buildSchemaHints(type, content) { if (!content || type !== "lesson") return []; return lintLessonContent(content, "reflect").findings.map((f) => `[${f.kind}] ${f.message}`); } /** The asset type of a maybe-ref, or `""` when it does not parse. */ function lenientRefType(ref) { if (!ref) return ""; try { return parseRefInput(ref).type; } catch { return ""; } } /** * A description derived from existing metadata (title, first heading, first * prose sentence) that passes `isValidDescription`, or `undefined`. Never * free-form invention. */ function deriveDescriptionFromAsset(title, body, targetRef) { const candidates = []; if (typeof title === "string" && title.trim()) candidates.push({ text: title.trim(), kind: "fragment" }); const heading = body.match(/^#{1,6}\s+(.+?)\s*$/m)?.[1]; if (heading) candidates.push({ text: heading.trim(), kind: "fragment" }); const sentence = firstProseSentence(body); if (sentence) candidates.push({ text: sentence, kind: "prose" }); for (const { text, kind } of candidates) { const normalized = text .replace(/`/g, "") .replace(/^[#>*\-\s]+/, "") .replace(/\s+/g, " ") .trim(); if (!normalized) continue; // A bare title/heading reads poorly as a description: prefer the sentence form. const variants = kind === "fragment" ? [`Reference notes on ${normalized}.`, normalized] : [normalized]; for (const v of variants) { const clamped = v.length > DESCRIPTION_MAX_CHARS ? v.slice(0, DESCRIPTION_MAX_CHARS).trimEnd() : v; if (isValidDescription(clamped, targetRef, { skipRefTailCheck: true }).ok) return clamped; } } return undefined; } function firstProseSentence(body) { for (const rawLine of body.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || /^(#{1,6}\s|```|~~~|[-*+]\s|\d+\.\s|>|\||<!--)/.test(line)) continue; return (line.match(/^(.+?[.!?])(\s|$)/)?.[1] ?? line).trim(); } return ""; } /** * The asset with the patch applied, or `undefined` when the patch changes * nothing (or there is no asset to patch). The body is the source's own, byte * for byte; the one thing akm adds to it is a `# title` heading it lacks. A * required description that neither the source nor the patch has is derived * from the asset's own text (#636). Only the changed keys' frontmatter lines are * rewritten: every other line is kept as it is, so a source whose YAML the * parser reads only in part (a broken description beside a list) loses nothing. */ export function applyReflectPatch(patch, sourceContent, targetRef) { if (!sourceContent.trim()) return undefined; const { fmText, body: sourceBody } = splitFrontmatter(sourceContent); // A fence that is opened and never closed (`---` fused onto the last value) // would leave the old block in the body under a new one: a person fixes it. if (fmText === null && /^---\r?\n/.test(sourceContent)) return undefined; const sourceFm = fmText !== null ? parseFrontmatter(sourceContent).data : {}; const { title, ...changes } = patch; const addTitle = title !== undefined && !/^#[ \t]+\S/m.test(sourceBody); const body = addTitle ? `# ${title}\n\n${sourceBody.replace(/^(\r?\n)+/, "")}` : sourceBody; // Only a source that already has frontmatter but no description gets one: // injecting a whole block, or overwriting an authored one, is out of scope. const refType = lenientRefType(targetRef); const desc = changes.description ?? sourceFm.description; if (refType && requiresDescription(refType) && (typeof desc !== "string" || desc.trim().length === 0) && Object.keys(sourceFm).length > 0) { const derived = deriveDescriptionFromAsset(sourceFm.title, body, targetRef); if (derived) changes.description = derived; } for (const key of Object.keys(changes)) if (changes[key] === sourceFm[key]) delete changes[key]; if (!addTitle && Object.keys(changes).length === 0) return undefined; // No frontmatter at all stays body-only unless the patch adds a field. The // blank line after a closing fence is part of the source body, so only a // block or heading akm adds brings its own. if (fmText === null) { if (Object.keys(changes).length === 0) return { content: body }; const content = `---\n${serializeFrontmatter(changes)}\n---\n\n${body}`; return { content, frontmatter: parseFrontmatter(content).data }; } const content = `---\n${patchFrontmatterLines(fmText, changes)}\n---\n${addTitle ? "\n" : ""}${body}`; return { content, frontmatter: parseFrontmatter(content).data }; } /** The frontmatter text with each changed key's lines (the key line and its indented continuation) replaced, or appended. */ function patchFrontmatterLines(fmText, changes) { const lines = fmText.split(/\r?\n/); for (const [key, value] of Object.entries(changes)) { const replacement = serializeFrontmatter({ [key]: value }).split("\n"); const start = lines.findIndex((line) => line.startsWith(`${key}:`)); if (start === -1) { lines.push(...replacement); continue; } let end = start + 1; while (end < lines.length && /^[ \t]/.test(lines[end] ?? "")) end++; lines.splice(start, end - start, ...replacement); } return lines.join("\n"); } // ── Output contract ────────────────────────────────────────────────────────── // // Every engine kind is asked for the same reply: the JSON object of // REFLECT_JSON_SCHEMA, or the framed-markdown frame for an LLM engine that // rejects JSON Schema. The reply is validated by the parse functions below and // repaired once, whatever engine produced it. const REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA = { type: "object", required: ["description", "when_to_use", "title"], additionalProperties: false, properties: { description: { type: ["string", "null"] }, when_to_use: { type: ["string", "null"] }, title: { type: ["string", "null"] }, }, }; const REFLECT_CONFIDENCE_SCHEMA = { type: "number", minimum: 0, maximum: 1, description: "Self-reported quality confidence in [0, 1]. Persisted on the proposal for reviewers and the triage judge to read during adjudication.", }; export const REFLECT_JSON_SCHEMA = { type: "object", required: ["confidence", "frontmatterPatch"], additionalProperties: false, properties: { confidence: REFLECT_CONFIDENCE_SCHEMA, frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA, }, }; const REFLECT_UNSCOPED_JSON_SCHEMA = { type: "object", required: ["ref", "confidence", "frontmatterPatch"], additionalProperties: false, properties: { ref: { type: "string", description: "Selected asset ref as a subdir-qualified conceptId." }, confidence: { type: "number", minimum: 0, maximum: 1, description: "Self-reported quality confidence in [0, 1]." }, frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA, }, }; /** * Frame for JSON Schema unless the LLM connection disabled it or already proved * this process that it rejects it (the transport retries plain text on a 4xx). */ function wantsJsonSchemaOutput(connection) { return connection.supportsJsonSchema !== false && !isJsonSchemaKnownUnsupported(connection); } /** Injected between the prior draft and the refinement request (self-refine). */ const REFLECT_CRITIQUE_PROMPT = "Your previous proposal is shown above. Review it critically and provide an improved version that is more specific, actionable, and avoids any issues with the previous attempt. Return only the improved response using the output contract from the original prompt."; function parsedRecord(result) { return result.parsed && typeof result.parsed === "object" && !Array.isArray(result.parsed) ? result.parsed : undefined; } function reflectTelemetry(result) { const parsed = parsedRecord(result); if (!parsed || (parsed.outputMode !== "json_schema" && parsed.outputMode !== "framed_markdown")) return undefined; if (typeof parsed.repairAttempts !== "number") return undefined; return { outputMode: parsed.outputMode, repairAttempts: parsed.repairAttempts }; } function parseReflectConfidence(value) { if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) { throw new Error('direct reflect response missing required number field "confidence" in [0, 1]'); } return value; } const REFLECT_PATCH_FIELDS = ["description", "when_to_use", "title"]; function parseReflectFrontmatterPatch(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error('direct reflect response missing required object field "frontmatterPatch"'); } const fields = value; if (Object.keys(fields).length !== REFLECT_PATCH_FIELDS.length || REFLECT_PATCH_FIELDS.some((f) => !(f in fields))) { throw new Error(`direct reflect frontmatterPatch fields must be exactly: ${REFLECT_PATCH_FIELDS.join(", ")}`); } const patch = {}; for (const field of REFLECT_PATCH_FIELDS) { const fieldValue = fields[field]; if (fieldValue === null) continue; if (typeof fieldValue !== "string" || !fieldValue.trim() || /[\r\n]/.test(fieldValue)) { throw new Error(`direct reflect frontmatterPatch.${field} must be a non-empty single-line string or null`); } patch[field] = fieldValue.trim(); } return patch; } function parseSchemaReflectOutput(raw, targetRef) { const parsed = parseEmbeddedJsonResponse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("direct reflect response was not valid JSON"); } const expectedKeys = targetRef ? ["confidence", "frontmatterPatch"] : ["confidence", "frontmatterPatch", "ref"]; const actualKeys = Object.keys(parsed).sort(); if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, index) => key !== expectedKeys[index])) { throw new Error(`direct reflect response fields must be exactly: ${expectedKeys.join(", ")}`); } const ref = targetRef ?? (typeof parsed.ref === "string" ? parsed.ref.trim() : ""); if (!ref) throw new Error('direct reflect response missing required string field "ref"'); return { ref, confidence: parseReflectConfidence(parsed.confidence), patch: parseReflectFrontmatterPatch(parsed.frontmatterPatch), }; } function parseFramedReflectOutput(raw, targetRef) { const headerLines = raw.replaceAll("\r\n", "\n").trim().split("\n").filter(Boolean); const header = (prefix) => headerLines.find((line) => line.startsWith(prefix)); const confidenceLine = header("AKM_REFLECT_CONFIDENCE:"); const refLine = header("AKM_REFLECT_REF:"); const patchLine = header("AKM_REFLECT_FRONTMATTER_PATCH:"); const invalidRefLine = targetRef ? refLine !== undefined : refLine === undefined; if (headerLines.length !== (targetRef ? 2 : 3) || !confidenceLine || !patchLine || invalidRefLine) { throw new Error("direct reflect response contained invalid frame metadata"); } const confidenceText = confidenceLine.slice("AKM_REFLECT_CONFIDENCE:".length).trim(); if (!/^(?:0(?:\.\d+)?|1(?:\.0+)?)$/.test(confidenceText)) { throw new Error("direct reflect frame confidence must be a decimal number in [0, 1]"); } const ref = targetRef ?? refLine?.slice("AKM_REFLECT_REF:".length).trim() ?? ""; if (!ref) throw new Error("direct reflect response contained an empty AKM_REFLECT_REF value"); let parsedPatch; try { parsedPatch = JSON.parse(patchLine.slice("AKM_REFLECT_FRONTMATTER_PATCH:".length).trim()); } catch { throw new Error("direct reflect response contained invalid frontmatter patch JSON"); } return { ref, confidence: parseReflectConfidence(Number(confidenceText)), patch: parseReflectFrontmatterPatch(parsedPatch), }; } /** * One reflect iteration on any engine, as an agent-shaped result (errors * captured, never thrown except configuration). Every engine kind is sent the * same request, with the reply's schema as the output schema, and its reply is * held to the same contract: an unparseable reply gets one repair turn within * the original deadline, then fails. A failed agent or SDK dispatch is reported * as it ran, with its exit code and stderr; an LLM call has only its message. */ export async function runReflectIteration(opts) { const start = Date.now(); let repairAttempts = 0; // Model work is bounded: with no timeout from the caller or the runner, the default. const configuredTimeout = Object.hasOwn(opts, "timeoutMs") ? (opts.timeoutMs ?? null) : Object.hasOwn(opts.runner, "timeoutMs") ? (opts.runner.timeoutMs ?? null) : DEFAULT_LLM_TIMEOUT_MS; const deadline = typeof configuredTimeout === "number" ? start + configuredTimeout : undefined; const messages = [{ role: "user", content: opts.prompt ?? "" }]; if (opts.priorDraft !== undefined && opts.iteration > 0) { messages.push({ role: "assistant", content: opts.priorDraft }, { role: "user", content: REFLECT_CRITIQUE_PROMPT }); } const parse = (raw) => opts.outputMode === "json_schema" ? parseSchemaReflectOutput(raw, opts.targetRef) : parseFramedReflectOutput(raw, opts.targetRef); const failure = (err, reason, stdout = "", exitCode = 1) => { const msg = errMessage(err); return { ok: false, stdout, stderr: msg, durationMs: Date.now() - start, exitCode, reason, error: msg, parsed: { outputMode: opts.outputMode, repairAttempts }, }; }; // A reply that broke the contract; its message is the parser's, on every engine kind. const invalidReply = (err, reply) => failure(err, "parse_error", reply, 0); // The result of the dispatch that failed, kept for an agent or SDK engine. let dispatched; // Reflect parses and repairs its own reply (the repair turn below), so one dispatch, unvalidated. const call = async (callMessages, repairTimeoutMs) => { const outcome = await callStageOnce({ feature: "reflect_proposal", runner: opts.runner, prompt: callMessages.at(-1)?.content ?? "", history: callMessages.slice(0, -1), request: { ...(repairTimeoutMs !== undefined ? { timeoutMs: repairTimeoutMs } : Object.hasOwn(opts, "timeoutMs") ? { timeoutMs: opts.timeoutMs } : {}), ...(opts.signal ? { signal: opts.signal } : {}), ...(opts.responseSchema !== undefined ? { responseSchema: opts.responseSchema } : {}), ...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}), // Visible chain-of-thought can exhaust the output before the envelope. enableThinking: false, ...(opts.chat ? { chat: opts.chat } : {}), ...(opts.runSdk ? { runSdk: opts.runSdk } : {}), ...(opts.runOptions ? { runOptions: opts.runOptions } : {}), }, ...(opts.environment ? { current: { environment: opts.environment } } : {}), ...(opts.onNotices ? { onNotices: opts.onNotices } : {}), }); if (!outcome.ok) { if (!runnerIsLlm(opts.runner)) dispatched = outcome.result; throw outcome.reason === "timeout" ? new LlmCallError(outcome.error ?? "timeout", "timeout") : new Error(outcome.error ?? "dispatch failed"); } return outcome.raw; }; try { if (opts.signal?.aborted) throw new Error("Reflect request aborted"); const stdout = await call(messages); let payload; let acceptedOutput = stdout; try { payload = parse(stdout); } catch (err) { if (opts.allowRepair === false) return invalidReply(err, stdout); if (opts.signal?.aborted) return failure(new Error("Reflect request aborted"), "aborted", stdout); const remaining = deadline === undefined ? undefined : deadline - Date.now(); if (remaining !== undefined && remaining <= 0) { return failure(new LlmCallError("Reflect request timed out before output repair", "timeout"), "timeout", stdout); } repairAttempts = 1; const repairPrompt = buildReflectOutputRepairPrompt(opts.outputMode, opts.targetRef !== undefined); acceptedOutput = await call([...messages, { role: "assistant", content: stdout }, { role: "user", content: repairPrompt }], remaining); try { payload = parse(acceptedOutput); } catch (repairErr) { return invalidReply(repairErr, acceptedOutput); } } return { ok: true, stdout: JSON.stringify(payload), stderr: "", durationMs: Date.now() - start, exitCode: 0, parsed: { outputMode: opts.outputMode, repairAttempts, priorDraft: acceptedOutput }, }; } catch (err) { if (err instanceof ConfigError) throw err; if (dispatched) return { ...dispatched, parsed: { outputMode: opts.outputMode, repairAttempts } }; const reason = opts.signal?.aborted ? "aborted" : err instanceof LlmCallError && err.code === "timeout" ? "timeout" : "non_zero_exit"; return failure(err, reason); } } /** The lazy `reflect_invoked` + failure-side `reflect_completed` emitters. */ function reflectEmitters(options) { let invoked = false; const emitInvoked = () => { if (invoked) return; appendEvent({ eventType: "reflect_invoked", ...(options.ref ? { ref: options.itemRef ?? options.ref } : {}), metadata: { ...(options.task ? { task: options.task } : {}), ...(options.engine ? { engine: options.engine } : {}), ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}), }, }, options.eventsCtx); invoked = true; }; const emitFailed = (reason, subreason, ref, extra) => { emitInvoked(); appendEvent({ eventType: "reflect_completed", ...(ref ? { ref } : {}), metadata: { source: "reflect", ok: false, reason, subreason, ...(extra ?? {}) }, }, options.eventsCtx); }; return { emitInvoked, emitFailed }; } /** A post-dispatch failure envelope (with the run's notices). */ function reflectFailure(run, result, reason, error, withOutput) { return { schemaVersion: 2, ok: false, reason, error, ...(run.options.ref ? { ref: run.options.ref } : {}), engine: run.engineName, exitCode: result.exitCode, ...(withOutput ? { stdout: result.stdout, ...(result.stderr ? { stderr: result.stderr } : {}) } : {}), ...run.notices.fields(), }; } function exitCodeMeta(result) { return result.exitCode !== null ? { exitCode: result.exitCode } : {}; } function unsupportedTypeFailure(ref, type, detail, emitFailed) { emitFailed("unsupported_type", "unsupported_type", ref, { type }); return { failure: { schemaVersion: 2, ok: false, reason: "unsupported_type", error: `Reflect refused: asset type "${type}" is not supported by reflect (${detail}). Use \`akm proposal new\` or edit the file directly.`, ref, exitCode: null, }, }; } /** The target's parsed ref and current content, or a refusal for a type reflect cannot patch. */ async function resolveReflectSource(options, stash, emitFailed) { if (!options.ref) return { assetContent: undefined, parsedRef: undefined }; const parsedRef = parseRefInput(options.ref); // A secret's content is never read, whatever it looks like. if (REFLECT_REFUSED_TYPES.has(parsedRef.type)) { return unsupportedTypeFailure(options.ref, parsedRef.type, "secret material is never read or sent to an LLM", emitFailed); } let assetContent = options.assetContent; if (assetContent === undefined) { try { const qualifiedRef = options.itemRef ?? options.ref; const localFilePath = await findAssetFilePath(qualifiedRef, stash); if (localFilePath && fs.existsSync(localFilePath)) { assetContent = fs.readFileSync(localFilePath, "utf8"); } else { const entry = await lookup(parseRefInput(qualifiedRef)); if (entry?.filePath && fs.existsSync(entry.filePath)) assetContent = fs.readFileSync(entry.filePath, "utf8"); } } catch { // An index miss is not fatal: reflect then has no content to patch. } } if (!REFLECT_ALLOWED_TYPES.has(parsedRef.type) && (assetContent === undefined || parseFrontmatter(assetContent).frontmatter === null)) { return unsupportedTypeFailure(options.ref, parsedRef.type, "its content is not frontmatter + markdown", emitFailed); } return { assetContent, parsedRef }; } /** * The single engine for this invocation: `--engine`, the improve strategy's * reflect process, or `defaults.engine` (announced when it falls back to the * SDK binary). Whatever its kind, reflect runs it under the model-work tool * policy. */ function resolveReflectRunner(options) { const config = options.config ?? loadConfig(); const activeStrategy = options.improveProfile ?? config.improve?.strategies?.[config.defaults?.improveStrategy ?? "default"]; const lower = (selection) => { const prepared = resolveExecution(selection); return buildExecution(prepared.request, prepared.runner); }; let lowered; if (options.engine) { lowered = lower({ content: "reflect engine selection", config, current: { engine: options.engine } }); } else if (options.improveProfile) { const resolved = resolveImproveExecution({ config, profile: activeStrategy, process: activeStrategy?.processes?.reflect, processName: "reflect", }); if (!resolved) { throw new ConfigError("Reflect requires an engine for the active improve strategy.", "LLM_NOT_CONFIGURED", "Set defaults.llmEngine or improve.strategies.<name>.processes.reflect.engine."); } lowered = resolved; } else { const { config: engineConfig, fallbackEngineName } = withEngineFallback(config); const defaultEngine = engineConfig.defaults?.engine; const announcement = fallbackAnnouncement(fallbackEngineName, defaultEngine); if (announcement) warn(announcement); if (!defaultEngine) { throw new ConfigError(`reflect ${NO_ENGINE_MESSAGE_SUFFIX} ${NO_ENGINE_REMEDY}`, "INVALID_CONFIG_FILE"); } lowered = lower({ content: "reflect engine selection", config }); } const runnerSpec = lowered.runner; const engineName = runnerSpec.engine ?? options.engine; if (!engineName) throw new ConfigError("Reflect requires a named engine.", "INVALID_CONFIG_FILE"); return { config, activeStrategy, runnerSpec, engineName, notices: lowered.notices }; } /** * Lower a runner under the model-work tool policy and check its credentials, * so a bad transport, or one that cannot confine the policy, fails before any work. */ function preflightReflectDispatch(runnerSpec, onNotices) { const prepared = resolveExecution({ content: "Validate reflect operation transport before dispatch.", runner: runnerSpec, modelWork: true, }); const lowered = buildExecution(prepared.request, prepared.runner); onNotices(lowered.notices); assertRunnerCredentials(lowered.runner); } /** * The flat 12k content cap exists for CLI argv; the HTTP runner can spend half * its context window (after the rest of the prompt) on the asset, reserving * the other half for the reply. Never below the flat floor. */ function computeReflectContentBudgetChars(promptInput, runnerSpec) { if (!runnerIsLlm(runnerSpec) || !promptInput.assetContent?.trim()) return undefined; const window = (runnerSpec.connection.contextLength ?? DEFAULT_CONTEXT_LENGTH_TOKENS) * CHARS_PER_TOKEN; const overhead = buildReflectPrompt({ ...promptInput, contentBudgetChars: 0 }).prompt.length; return Math.max(REFLECT_CONTENT_CAP, Math.floor((window - overhead) / 2)); } /** Every read-only prompt input, shared by dispatch and `--show-prompt`. */ function gatherReflectPromptSources(options, stash, parsedRef, assetContent) { return { feedback: readRecentFeedback(options.ref ? (options.itemRef ?? options.ref) : undefined, options.eventsCtx), schemaHints: buildSchemaHints(parsedRef?.type ?? "", assetContent), rejectedProposals: rejectedProposalContext(stash, options.ref, options.ctx, options.eventsCtx), standardsContext: resolveStandardsContext(options.ref, stash), }; } /** The exact prompt reflect sends, shared by dispatch and `--show-prompt`. */ function buildReflectPromptText(args) { const { options, parsedRef, assetContent, sources, runnerSpec, priorDraft } = args; const { feedback, schemaHints, rejectedProposals, standardsContext } = sources; // An LLM engine that rejects JSON Schema gets the framed contract; every other engine gets the JSON object. const outputMode = runnerIsLlm(runnerSpec) && !wantsJsonSchemaOutput(runnerSpec.connection) ? "framed_markdown" : "json_schema"; const input = { ...(options.ref ? { ref: options.ref } : {}), ...(parsedRef?.type ? { type: parsedRef.type } : {}), ...(parsedRef?.name ? { name: parsedRef.name } : {}), ...(assetContent !== undefined ? { assetContent } : {}), ...(feedback.length > 0 ? { feedback } : {}), ...(schemaHints.length > 0 ? { schemaHints } : {}), ...(options.task ? { task: options.task } : {}), ...(standardsContext.trim() ? { standardsContext } : {}), ...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}), ...(rejectedProposals.length > 0 ? { rejectedProposals } : {}), ...(priorDraft !== undefined ? { priorDraft } : {}), outputMode, }; const contentBudgetChars = computeReflectContentBudgetChars(input, runnerSpec); const { prompt } = buildReflectPrompt({ ...input, ...(contentBudgetChars !== undefined ? { contentBudgetChars } : {}), }); return { prompt, outputMode }; } /** * Dispatch with the optional self-refine loop: up to `maxRefineIters` passes, * each critiquing the prior draft, stopping early on an unchanged draft. Every * engine kind runs the same iteration, and the repair budget is shared across * passes. Under the model-work tool policy an agent can edit only its own * scratch directory, which is gone once it returns, so it returns the proposal * as its reply, as an LLM does. */ async function runReflectRefineIterations(args) { const { run, parsedRef, assetContent, sources, agentEnv } = args; const { options, runnerSpec } = run; const maxRefineIters = Math.max(1, options.maxRefineIters ?? 1); let result = {}; let priorDraft; let repairAttempts = 0; // Only an agent or SDK engine runs a child process, with an environment and spawn or SDK seams. const childProcess = runnerIsLlm(runnerSpec) ? {} : { ...(Object.keys(agentEnv).length > 0 ? { environment: agentEnv } : {}), ...(options.runSdk ? { runSdk: options.runSdk } : {}), ...(options.runAgentOptions ? { runOptions: options.runAgentOptions } : {}), }; for (let iter = 0; iter < maxRefineIters; iter++) { const { prompt, outputMode } = buildReflectPromptText({ options, parsedRef, assetContent, sources, runnerSpec, priorDraft, }); const iterResult = await runReflectIteration({ prompt, runner: runnerSpec, ...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}), ...(options.signal ? { signal: options.signal } : {}), priorDraft, iteration: iter, ...(outputMode === "json_schema" ? { responseSchema: options.ref ? REFLECT_JSON_SCHEMA : REFLECT_UNSCOPED_JSON_SCHEMA } : {}), outputMode, ...(options.ref ? { targetRef: options.ref } : {}), allowRepair: repairAttempts === 0, ...(options.chat ? { chat: options.chat } : {}), ...childProcess, onNotices: run.notices.add, }); const telemetry = reflectTelemetry(iterResult); if (telemetry) repairAttempts += telemetry.repairAttempts; result = telemetry ? { ...iterResult, parsed: { ...iterResult.parsed, ...telemetry, repairAttempts } } : iterResult; if (!result.ok) break; if (iter < maxRefineIters - 1) { const priorFromReply = parsedRecord(result)?.priorDraft; const nextDraft = typeof priorFromReply === "string" ? priorFromReply : (result.stdout ?? ""); if (priorDraft !== undefined && nextDraft === priorDraft) break; priorDraft = nextDraft; } } return result; } const NOISE_SUBREASONS = { noop: "reflect_skipped_noop", cosmetic: "reflect_skipped_cosmetic", "low-value": "reflect_skipped_low_value", }; /** * Apply the patch, drop a no-op/cosmetic (and optionally low-value) change, * judge the exact content that would be persisted, then mint. A revision with a * deterministic defect is refused before the judge runs, whether or not the * gate is on. */ async function finalizeReflectProposal(args) { const { run, payload, assetContent, result, judge, feedback } = args; const { options } = run; const telemetry = reflectTelemetry(result) ?? {}; const patched = applyReflectPatch(payload.patch, assetContent, payload.ref); // A patch that changes nothing leaves the asset as it is: an empty diff. const content = patched?.content ?? assetContent; const changeKind = classifyReflectChange(assetContent, content); if (changeKind === "noop" || changeKind === "cosmetic" || (changeKind === "low-value" && options.lowValueFilter === true)) { run.emitFailed("no_change", NOISE_SUBREASONS[changeKind], options.ref, { changeKind, ...telemetry }); const what = changeKind === "noop" ? "identical to the current asset (empty diff)" : changeKind === "low-value" ? "a low-value prose micro-rewrite (few changed tokens, no structural changes)" : "a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes)"; return reflectFailure(run, result, "no_change", `Reflect skipped: proposed content for ${payload.ref} is ${what}; no proposal created.`, false); } /** A judge refused the revision: record it for the ledger's rejection window and stop. */ const refuse = (detail, metadata, message) => { if (options.ref) { recordLedgerAttempt({ proposalsCtx: options.ctx, eventsCtx: options.eventsCtx }, { stashDir: run.stash, ref: options.itemRef ?? options.ref, source: "reflect", outcome: "quality_rejected", detail, }); } appendEvent({ eventType: "reflect_completed", ref: payload.ref, metadata: { source: "reflect", qualityRejected: true, ...metadata, ...telemetry }, }, options.eventsCtx); return reflectFailure(run, result, "quality_rejected", message, false); }; // A defect no judge needs to weigh is refused before any judge call, whether or not the gate is on. const defect = findReflectDefect(assetContent, content, options.defectFilter); if (defect) return refuse(defect, { reflectDefect: defect }, `Reflect proposal refused before the judge: ${defect}`); let verdict; let judgeFailed = false; if (judge.enabled) { verdict = await runReflectQualityJudge(run.config, content, assetContent, feedback, options.chat, { runnerSelectionFrozen: true, ref: payload.ref, ...(judge.runner ? { llmRunner: judge.runner } : {}), ...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}), ...(options.signal ? { signal: options.signal } : {}), onNotices: run.notices.add, }); // A judge that timed out, errored or replied unparseably gave no verdict: a person reviews the revision. judgeFailed = verdict.reviewNeeded === true && verdict.score === -1; if (!verdict.pass && !judgeFailed) { return refuse(verdict.reason, { qualityScore: verdict.score, qualityReason: verdict.reason, ...(verdict.criteria ? { qualityCriteria: verdict.criteria } : {}), }, `Reflect proposal quality gate rejected: score=${verdict.score}, reason="${verdict.reason}"`); } } // #722: a revision of an existing asset must not grade lower on its own retrieval queries. if (verdict?.pass && judge.runner) { const retrieval = await runRetrievalRegressionGate({ ref: payload.ref, before: assetContent, after: content, queries: loadRetrievalQueries({ proposalsCtx: options.ctx, eventsCtx: options.eventsCtx }, payload.ref), runner: judge.runner, ...(options.chat ? { chat: options.chat } : {}), ...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}), ...(options.signal ? { signal: options.signal } : {}), onNotices: run.notices.add, }); if (!retrieval.pass) { return refuse(retrieval.reason, { retrievalRegression: true, retrievalQueries: retrieval.queries, ...(retrieval.oldMean !== undefined ? { retrievalGradeBefore: retrieval.oldMean } : {}), ...(retrieval.newMean !== undefined ? { retrievalGradeAfter: retrieval.newMean } : {}), }, `Reflect proposal refused: ${retrieval.reason}`); } } const reviewReasons = [ ...(judge.skippedNoJudge ? ["no-judge-configured"] : []), ...(judgeFailed ? ["judge-error"] : []), ]; const proposal = mintProposal(run.stash, options.ctx, { ref: payload.ref, ...(options.target ? { target: options.target } : {}), source: "reflect", sourceRun: `reflect-${Date.now()}`, payload: { content, ...(patched?.frontmatter ? { frontmatter: patched.frontmatter } : {}) }, confidence: payload.confidence, ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}), ...(options.itemRef ? { itemRef: options.itemRef, attemptedRefs: [options.itemRef] } : {}), }, reviewReasons.length > 0 ? { review: { reason: reviewReasons.join("+"), // The quality gate's hand-off to a person, as distill's: the triage drain leaves it alone. gate: judgeFailed ? "quality-gate" : "reflect", }, } : { judged: verdict }); appendEvent({ eventType: "reflect_completed", ref: proposal.ref, metadata: { proposalId: proposal.id, source: "reflect", engine: run.engineName, ...(judge.skippedNoJudge ? { qualityGateSkippedNoJudge: true } : {}), ...(judgeFailed ? { qualityReason: verdict?.reason } : {}), ...telemetry, }, }, options.eventsCtx); return { schemaVersion: 2, ok: true, proposal, ref: proposal.ref, engine: run.engineName, durationMs: result.durationMs, ...run.notices.fields(), }; } /** * `akm improve <ref> --show-prompt`: the exact prompt reflect would send for * one asset. Read-only: no credential, no dispatch, no event. */ export async function renderReflectPromptPreview(options) { if (!options.ref) { throw new UsageError("renderReflectPromptPreview requires options.ref.", "INVALID_FLAG_VALUE"); } const ref = options.ref; const stash = options.stashDir ?? resolveStashDir(); const source = await resolveReflectSource(options, stash, () => { }); if ("failure" in source) { const { failure } = source; throw new UsageError((!failure.ok && failure.error) || `Reflect cannot preview ref "${ref}".`, "INVALID_FLAG_VALUE"); } const { runnerSpec, engineName } = resolveReflectRunner(options); const sources = gatherReflectPromptSources(options, stash, source.parsedRef, source.assetContent); const { prompt } = buildReflectPromptText({ options, parsedRef: source.parsedRef, assetContent: source.assetContent, sources, runnerSpec, priorDraft: undefined, }); return { ref, prompt, engine: engineName, engineKind: runnerSpec.kind }; } export async function akmReflect(options = {}) { const stash = options.stashDir ?? resolveStashDir(); const { emitInvoked, emitFailed } = reflectEmitters(options); const source = await resolveReflectSource(options, stash, emitFailed); if ("failure" in source) return source.failure; const { assetContent, parsedRef } = source; const { config, activeStrategy, runnerSpec, engineName, notices: resolutionNotices } = resolveReflectRunner(options); const notices = noticeSet(); notices.add(resolutionNotices); const run = { options, stash, config, runnerSpec, engineName, notices, emitInvoked, emitFailed }; // Judge selection is frozen before dispatch so a missing judge credential fails first. const judgeWanted = activeStrategy?.processes?.reflect?.qualityGate?.enabled ?? true; let judgeRunner; if (judgeWanted) { const gateJudge = resolveQualityGateJudge(config, activeStrategy, "reflect", notices.add); if (gateJudge) { judgeRunner = gateJudge; } else if (runnerIsLlm(runnerSpec)) { judgeRunner = runnerSpec; } else { const resolved = resolveImproveExecution({ config, processName: "reflect_proposal_quality-judge" }); if (resolved) notices.add(resolved.notices); judgeRunner = resolved?.runner; } } const skippedNoJudge = judgeWanted && !judgeRunner; if (skippedNoJudge) { warnOnce("reflect-quality-gate-no-judge", "Reflect proposal quality gate has no engine configured to judge proposals (set defaults.llmEngine). Skipping the gate for this run; the proposal is queued for human review instead."); } preflightReflectDispatch(runnerSpec, notices.add); if (judgeRunner && judgeRunner !== runnerSpec) preflightReflectDispatch(judgeRunner, notices.add); const sources = gatherReflectPromptSources(options, stash, parsedRef, assetContent); const agentEnv = options.eventSource === "improve" ? { AKM_EVENT_SOURCE: "improve" } : {}; const sensitiveValues = collectDispatchSensitiveValues(runnerSpec, { ...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}), ...(options.runAgentOptions ?? {}), }); let result; let payload; try { result = await runReflectRefineIterations({ run, parsedRef, assetContent, sources, agentEnv }); emitInvoked(); if (!result.ok) { if (isEnoentFailure(result)) { emitFailed("spawn_failed", "enoent", options.ref, { ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}), }); return { ...baseFailureFields(result), schemaVersion: 2, ...(options.ref ? { ref: options.ref } : {}), engine: engineName, error: enoentHintMessage(runnerIsLlm(runnerSpec) ? engineName : runnerSpec.profile.bin), ...notices.fields(), }; } const envelope = { ...baseFailureFields(result), schemaVersion: 2, ...(options.ref ? { ref: options.ref } : {}), engine: engineName, }; emitFailed(envelope.reason, envelope.reason === "parse_error" ? "parse_error" : "agent_crash", options.ref, { ...(envelope.exitCode !== null ? { exitCode: envelope.exitCode } : {}), ...(reflectTelemetry(result) ?? {}), }); return { ...envelope, ...notices.fields() }; } // The iteration parsed the reply and put its payload on stdout. payload = JSON.parse(result.stdout); } catch (error) { if (!(error instanceof ConfigError)) emitInvoked(); throw error; } const generated = Object.values(payload.patch).join("\n"); const unsafeContent = generatedContentRejection(generated, redactSensitiveText(generated, sensitiveValues)); if (unsafeContent) { emitFailed("parse_error", "parse_error", options.ref, exitCodeMeta(result)); return reflectFailure(run, result, "parse_error", unsafeContent, false); } // An unscoped reply names its asset only now: read it as a targeted run read its own before dispatch. const target = options.ref ? { assetContent } : await resolveReflectSource({ ...options, ref: payload.ref }, stash, emitFailed); if ("failure" in target) return target.failure; return finalizeReflectProposal({ run, payload, assetContent: target.assetContent ?? "", result, judge: { enabled: judgeWanted && !skippedNoJudge, skippedNoJudge, runner: judgeRunner }, feedback: sources.feedback, }); }