UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

655 lines (603 loc) • 25.8 kB
import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { basename, dirname, join, relative, resolve } from "node:path"; import type { RouteSwitchRecord } from "./route-switch.ts"; import { computeDecayedWeight, type RoutingRecord, type RoutingRecordV2 } from "./routing.ts"; import { nowIso } from "./protocol.ts"; import { compareCodeUnitIds } from "./relevance.ts"; export const CANDIDATE_SCHEMA = "kxm.candidate.v1" as const; export const IMPROVEMENT_REPORT_SCHEMA = "kxm.improvement-report.v2" as const; export const IMPROVEMENT_REPORT_V1_SCHEMA = "kxm.improvement-report.v1" as const; export type CandidateKind = "gate" | "skill" | "workflow-step"; export interface CandidateBaselineMetrics { recurrence: number; meanCost: number; meanLatency: number; verifyPassRate: number; rework: number; } export interface ImprovementCandidate { schema: typeof CANDIDATE_SCHEMA; id: string; kind: CandidateKind; summary: string; evidenceRefs: string[]; baselineMetrics: CandidateBaselineMetrics; declaredOutcome: string; measure: string; proposedDiffPath: string; status: "proposed" | "evaluating" | "review-required" | "promoted" | "rejected"; createdAt: string; } /** Why a group that recurs is still not a coded-repeat candidate. */ export type ImprovementExcludedReason = "writes-repository" | "ask-not-repeated"; export interface ImprovementGroupRow { workflowHash: string; /** The engine's workflow id when the records carry one. */ workflowId?: string; stepId: string; agentRole: string; promptHash: string; recurrence: number; /** Distinct runs among decided records. */ distinctRuns: number; /** Most distinct decided runs that shared one objective (the same ask). */ askRecurrence: number; undecidedRecords: number; meanCost: number; meanLatency: number; /** Accepted share of decided records; a superseded retry never passes. */ verifyPassRate: number; rework: number; /** Recency-weighted recurrence. Orders rows; never decides candidacy. */ weightedRecurrence: number; undatedRecords: number; costSamples: number; writesRepository: boolean; evidenceRefs: string[]; isCandidate: boolean; excludedReason?: ImprovementExcludedReason; candidateKind?: CandidateKind; candidateId?: string; } export interface ImprovementPromotionEntry extends PromotionReadiness { candidateId: string; } export interface ImprovementReport { schema: typeof IMPROVEMENT_REPORT_SCHEMA; createdAt: string; reviewDecision: "proposed"; recordsCount: number; groups: ImprovementGroupRow[]; candidates: ImprovementCandidate[]; promotionPolicy: string; promotion: ImprovementPromotionEntry[]; routeSwitches?: RouteSwitchRecord[] | undefined; } export function classifyCandidateKind(stepId: string, agentRole: string): CandidateKind { const s = stepId.toLowerCase(); const r = agentRole.toLowerCase(); if (s.includes("verify") || s.includes("gate") || s.includes("test") || s.includes("check") || s.includes("lint") || r === "verifier") { return "gate"; } if (s.includes("plan") || s.includes("review") || s.includes("repro") || r === "planner" || r === "reviewer" || r === "critic") { return "skill"; } return "workflow-step"; } function candidateSlug(value: string): string { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "step"; } function generateCandidateDiff( kind: CandidateKind, stepId: string, agentRole: string, workflowId?: string | undefined, ): { diff: string; declaredOutcome: string; measure: string; summary: string } { const safeSlug = candidateSlug(stepId); if (kind === "gate") { const diff = [ "diff --git a/.kxm/gates.yaml b/.kxm/gates.yaml", "--- a/.kxm/gates.yaml", "+++ b/.kxm/gates.yaml", "@@ -1,3 +1,7 @@", " schema: kxm.gate-registry.v1", " gates:", `+ ${safeSlug}:`, `+ kind: command`, `+ argv: [npm, run, ${safeSlug}]`, `+ timeoutMs: 3600000`, "", ].join("\n"); return { diff, declaredOutcome: `Deterministic exit-code verification replacing LLM turn for ${stepId}`, measure: `100% reduction in LLM inference cost and latency for ${stepId}`, summary: `Promote deterministic check '${stepId}' (${agentRole}) to coded gate`, }; } if (kind === "skill") { const rel = `.kxm/skills/candidates/${safeSlug}/SKILL.md`; const skillContent = [ "---", `name: ${safeSlug}`, `description: Governed repeat skill for ${stepId}`, "---", "", `# ${stepId}`, "", `Reusable skill instructions for ${stepId} (${agentRole}).`, "", ].join("\n"); const diff = [ `diff --git a/${rel} b/${rel}`, "new file mode 100644", "--- /dev/null", `+++ b/${rel}`, `@@ -0,0 +1,${skillContent.split("\n").length} @@`, ...skillContent.split("\n").map((l) => `+${l}`), "", ].join("\n"); return { diff, declaredOutcome: `Governed reusable skill for ${stepId}`, measure: `Reduced prompt drift and consistent model guidance for ${stepId}`, summary: `Consolidate repeated instructions for '${stepId}' (${agentRole}) into a governed skill (consolidation, not a coded step)`, }; } // A workflow-step candidate proposes the coded step itself: the agent turn // becomes a command gate the operator writes and reviews. const rel = `.kxm/workflows/${workflowId !== undefined ? candidateSlug(workflowId) : safeSlug}.yaml`; const diff = [ `diff --git a/${rel} b/${rel}`, `--- a/${rel}`, `+++ b/${rel}`, "@@ -1,3 +1,3 @@", ` - id: ${stepId}`, "- kind: agent", `- agent: ${agentRole}`, "+ kind: gate", `+ gate: ${safeSlug}`, "diff --git a/.kxm/gates.yaml b/.kxm/gates.yaml", "--- a/.kxm/gates.yaml", "+++ b/.kxm/gates.yaml", "@@ -1,2 +1,6 @@", " schema: kxm.gate-registry.v1", " gates:", `+ ${safeSlug}:`, "+ kind: command", `+ argv: [node, scripts/${safeSlug}.mjs]`, "+ timeoutMs: 600000", "", ].join("\n"); return { diff, declaredOutcome: `Coded gate replaces the model turn for ${stepId}; the operator writes scripts/${safeSlug}.mjs`, measure: `Model cost and latency for ${stepId} drop to zero while its accepted rate holds`, summary: `Replace the model turn for '${stepId}' (${agentRole}) with a coded gate step`, }; } /** A trimmed, non-empty string, or undefined. */ function nonEmptyText(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function roundThree(value: number): number { return Number(value.toFixed(3)); } interface GroupAccumulator { key: string; workflowHash: string; workflowId?: string; stepId: string; agentRole: string; promptHash: string; costs: number[]; latencies: number[]; total: number; decided: number; passed: number; undecided: number; reworkCount: number; weight: number; undated: number; writes: boolean; decidedRuns: Set<string>; /** Decided runs per objective digest; one '(unkeyed)' bucket for records without one. */ askRuns: Map<string, Set<string>>; evidenceRefs: Set<string>; } export interface GroupRoutingOptions { minRecurrence?: number | undefined; minPassRate?: number | undefined; /** improvement.telemetryHalfLifeDays; weights order rows and never decide candidacy. */ halfLifeDays?: number | undefined; /** Epoch milliseconds the recency weights are measured against. */ now?: number | undefined; } /** * Group routing records by (workflowHash, step, role, promptHash) across runs. * * Identity: workflowHash is the engine's workflowId when present, then a * workflow definition digest, then the run; promptHash is the engine's * askSha256 (stable across runs, independent of the objective), then a v1 * rolePromptSha256. A group is a coded-repeat candidate only when the same * objective was decided in at least minRecurrence runs, the accepted rate over * decided records reaches minPassRate, and the step writes no repository. */ export function groupRoutingRecords( records: Array<RoutingRecord | RoutingRecordV2>, options: GroupRoutingOptions = {}, ): ImprovementGroupRow[] { const minRecurrence = options.minRecurrence ?? 2; const minPassRate = options.minPassRate ?? 0.75; const halfLifeDays = options.halfLifeDays ?? 14; const now = options.now ?? Date.now(); const raws = records.map((record) => record as unknown as Record<string, unknown>); const runKeys = raws.map((raw, index) => nonEmptyText(raw.runId) ?? nonEmptyText(raw.workflowRunId) ?? `record:${index}`); const stepIds = raws.map((raw) => nonEmptyText(raw.stepId) ?? nonEmptyText(raw.stageId) ?? "unknown"); const retriesOf = (raw: Record<string, unknown>): number => typeof raw.retries === "number" && Number.isFinite(raw.retries) ? raw.retries : 0; // A retry of the same step in the same run supersedes every earlier attempt. const maxRetries = new Map<string, number>(); raws.forEach((raw, index) => { const attemptKey = `${runKeys[index]}\u0000${stepIds[index]}`; maxRetries.set(attemptKey, Math.max(maxRetries.get(attemptKey) ?? 0, retriesOf(raw))); }); const map = new Map<string, GroupAccumulator>(); raws.forEach((raw, index) => { const providerMeta = raw.providerMetadata && typeof raw.providerMetadata === "object" && !Array.isArray(raw.providerMetadata) ? raw.providerMetadata as Record<string, unknown> : {}; const workflowId = nonEmptyText(providerMeta.workflowId); const workflowHash = workflowId ?? nonEmptyText(raw.workflowDefinitionSha256) ?? nonEmptyText(providerMeta.workflowDefinitionSha256) ?? nonEmptyText(raw.workflowRunId) ?? nonEmptyText(raw.runId) ?? "standalone"; const stepId = stepIds[index]!; const agentRole = nonEmptyText(raw.agentRole) ?? "agent"; const promptHash = nonEmptyText(providerMeta.askSha256) ?? nonEmptyText(raw.rolePromptSha256) ?? "none"; const runKey = runKeys[index]!; const key = `${workflowHash}:${stepId}:${agentRole}:${promptHash}`; let group = map.get(key); if (!group) { group = { key, workflowHash, stepId, agentRole, promptHash, costs: [], latencies: [], total: 0, decided: 0, passed: 0, undecided: 0, reworkCount: 0, weight: 0, undated: 0, writes: false, decidedRuns: new Set<string>(), askRuns: new Map<string, Set<string>>(), evidenceRefs: new Set<string>(), }; map.set(key, group); } if (group.workflowId === undefined && workflowId !== undefined) group.workflowId = workflowId; group.total += 1; const cost = raw.costUsd; if (typeof cost === "number" && !Number.isNaN(cost) && cost >= 0) { group.costs.push(cost); } const latency = raw.latencyMs; if (typeof latency === "number" && !Number.isNaN(latency) && latency >= 0) { group.latencies.push(latency); } const verifierOutcome = raw.verifierOutcome; const finalOutcome = raw.finalOutcome; const decided = (verifierOutcome !== undefined && verifierOutcome !== null) || (finalOutcome !== undefined && finalOutcome !== null && finalOutcome !== "pending"); if (decided) { group.decided += 1; group.decidedRuns.add(runKey); const ask = nonEmptyText(providerMeta.objectiveSha256) ?? "(unkeyed)"; let askRuns = group.askRuns.get(ask); if (!askRuns) { askRuns = new Set<string>(); group.askRuns.set(ask, askRuns); } askRuns.add(runKey); const superseded = (maxRetries.get(`${runKey}\u0000${stepId}`) ?? 0) > retriesOf(raw); if (!superseded && (verifierOutcome === "passed" || finalOutcome === "accepted" || finalOutcome === "completed")) { group.passed += 1; } } else { group.undecided += 1; } const retries = raw.retries; if (typeof retries === "number") { group.reworkCount += retries; } const recordedAt = nonEmptyText(raw.recordedAt); if (recordedAt === undefined || !Number.isFinite(Date.parse(recordedAt))) group.undated += 1; group.weight += computeDecayedWeight(recordedAt ?? "", halfLifeDays, now); if (providerMeta.stepWrites === true) group.writes = true; const ref = (raw.attemptId as string) || (raw.runId as string) || (raw.workflowRunId as string) || (raw.behavioralSha256 as string); if (ref && group.evidenceRefs.size < 16) { group.evidenceRefs.add(ref); } }); const keyed: Array<{ key: string; row: ImprovementGroupRow }> = []; for (const group of map.values()) { const recurrence = group.total; const meanCost = group.costs.length > 0 ? Number((group.costs.reduce((a, b) => a + b, 0) / group.costs.length).toFixed(4)) : 0; const meanLatency = group.latencies.length > 0 ? Math.round(group.latencies.reduce((a, b) => a + b, 0) / group.latencies.length) : 0; const verifyPassRate = group.decided > 0 ? roundThree(group.passed / group.decided) : 0; const distinctRuns = group.decidedRuns.size; const askRecurrence = Math.max(0, ...[...group.askRuns.values()].map((runs) => runs.size)); const passes = verifyPassRate >= minPassRate; const isCandidate = askRecurrence >= minRecurrence && passes && !group.writes; let excludedReason: ImprovementExcludedReason | undefined; if (!isCandidate && passes) { if (askRecurrence >= minRecurrence && group.writes) excludedReason = "writes-repository"; else if (distinctRuns >= minRecurrence && askRecurrence < minRecurrence) excludedReason = "ask-not-repeated"; } const candidateKind = isCandidate ? classifyCandidateKind(group.stepId, group.agentRole) : undefined; const safeSlug = group.stepId.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 16) || "step"; const keyHash = createHash("sha256").update(`${group.workflowHash}:${group.stepId}:${group.agentRole}:${group.promptHash}`).digest("hex").slice(0, 10); const candidateId = isCandidate && candidateKind ? `cand_${candidateKind.replace(/-/g, "_")}_${safeSlug}_${keyHash}` : undefined; keyed.push({ key: group.key, row: { workflowHash: group.workflowHash, ...(group.workflowId !== undefined ? { workflowId: group.workflowId } : {}), stepId: group.stepId, agentRole: group.agentRole, promptHash: group.promptHash, recurrence, distinctRuns, askRecurrence, undecidedRecords: group.undecided, meanCost, meanLatency, verifyPassRate, rework: group.reworkCount, weightedRecurrence: roundThree(group.weight), undatedRecords: group.undated, costSamples: group.costs.length, writesRepository: group.writes, evidenceRefs: [...group.evidenceRefs], isCandidate, ...(excludedReason !== undefined ? { excludedReason } : {}), ...(candidateKind !== undefined ? { candidateKind } : {}), ...(candidateId !== undefined ? { candidateId } : {}), }, }); } return keyed .sort((left, right) => { const a = left.row; const b = right.row; if (a.isCandidate !== b.isCandidate) return a.isCandidate ? -1 : 1; if (b.weightedRecurrence !== a.weightedRecurrence) return b.weightedRecurrence - a.weightedRecurrence; if (b.recurrence !== a.recurrence) return b.recurrence - a.recurrence; return compareCodeUnitIds(a.stepId, b.stepId) || compareCodeUnitIds(left.key, right.key); }) .map((entry) => entry.row); } export interface BuildImprovementReportOptions extends GroupRoutingOptions { candidatesDir?: string | undefined; projectRoot?: string | undefined; dryRun?: boolean | undefined; /** improvement.promotionPolicy; reported as readiness only. */ promotionPolicy?: string | undefined; autoThreshold?: PromotionAutoThreshold | undefined; routeSwitches?: readonly RouteSwitchRecord[] | undefined; } export function buildImprovementReport( records: Array<RoutingRecord | RoutingRecordV2>, options: BuildImprovementReportOptions = {}, ): ImprovementReport { const projectRoot = options.projectRoot ? resolve(options.projectRoot) : process.cwd(); const candidatesDir = options.candidatesDir ? resolve(options.candidatesDir) : join(projectRoot, ".kxm", "candidates"); const promotionPolicy = options.promotionPolicy ?? "manual_pr"; const groups = groupRoutingRecords(records, options); const candidates: ImprovementCandidate[] = []; const promotion: ImprovementPromotionEntry[] = []; for (const group of groups) { if (!group.isCandidate || !group.candidateKind || !group.candidateId) continue; const { diff, declaredOutcome, measure, summary } = generateCandidateDiff( group.candidateKind, group.stepId, group.agentRole, group.workflowId, ); const diffFileName = `${group.candidateId}.diff`; const diffFilePath = join(candidatesDir, diffFileName); const relDiffPath = relative(projectRoot, diffFilePath).replace(/\\/g, "/"); const candidate: ImprovementCandidate = { schema: CANDIDATE_SCHEMA, id: group.candidateId, kind: group.candidateKind, summary, evidenceRefs: group.evidenceRefs.length > 0 ? group.evidenceRefs : ["evidence:telemetry"], baselineMetrics: { recurrence: group.recurrence, meanCost: group.meanCost, meanLatency: group.meanLatency, verifyPassRate: group.verifyPassRate, rework: group.rework, }, declaredOutcome, measure, proposedDiffPath: relDiffPath, status: "proposed", createdAt: nowIso(), }; if (!options.dryRun) { if (!existsSync(candidatesDir)) { mkdirSync(candidatesDir, { recursive: true }); } writeFileSync(diffFilePath, diff, "utf8"); const jsonFilePath = join(candidatesDir, `${group.candidateId}.json`); writeFileSync(jsonFilePath, JSON.stringify(candidate, null, 2) + "\n", "utf8"); } candidates.push(candidate); } const rowsByCandidate = new Map(groups.flatMap((row) => row.candidateId !== undefined ? [[row.candidateId, row] as const] : [])); for (const candidate of candidates) { const row = rowsByCandidate.get(candidate.id); promotion.push({ candidateId: candidate.id, ...evaluatePromotionPolicy(candidate, promotionPolicy, { autoThreshold: options.autoThreshold, ...(row ? { costSamples: row.costSamples, distinctRuns: row.distinctRuns } : {}), }), }); } const routeSwitches = options.routeSwitches?.filter((item) => item.from && item.to) ?? []; return { schema: IMPROVEMENT_REPORT_SCHEMA, createdAt: nowIso(), reviewDecision: "proposed", recordsCount: records.length, groups, candidates, promotionPolicy, promotion, ...(routeSwitches.length > 0 ? { routeSwitches: [...routeSwitches] } : {}), }; } export function writeImprovementReport(improvementsDir: string, report: ImprovementReport, dryRun = false): string { const stamp = report.createdAt.replace(/[:.]/g, "-"); const path = join(improvementsDir, `${stamp}.json`); if (!dryRun) { mkdirSync(improvementsDir, { recursive: true }); writeFileSync(path, `${JSON.stringify(report, null, 2)}\n`, { encoding: "utf8" }); } return path; } export function formatImprovementReport(report: ImprovementReport): string { const lines: string[] = [ `Improvement Report (${report.recordsCount} record(s), ${report.groups.length} group(s), ${report.candidates.length} candidate(s); promotion policy ${report.promotionPolicy})`, "", "Workflow Step Role Prompt Records Runs Asks Weighted Cost ($) Latency (ms) Accepted Rework Candidate", "------------------------------------------------------------------------------------------------------------------------------------------", ]; for (const g of report.groups) { const wf = g.workflowHash.slice(0, 12).padEnd(14); const step = g.stepId.slice(0, 11).padEnd(12); const role = g.agentRole.slice(0, 11).padEnd(12); const prompt = g.promptHash.replace(/^sha256:/, "").slice(0, 10).padEnd(12); const rec = String(g.recurrence).padStart(7); const runs = String(g.distinctRuns).padStart(5); const asks = String(g.askRecurrence).padStart(5); const weighted = g.weightedRecurrence.toFixed(3).padStart(9); const cost = g.meanCost.toFixed(3).padStart(9); const lat = String(g.meanLatency).padStart(13); const accepted = `${(g.verifyPassRate * 100).toFixed(0)}%`.padStart(9); const rework = String(g.rework).padStart(7); const cand = g.isCandidate ? `yes (${g.candidateKind})` : g.excludedReason !== undefined ? `no (${g.excludedReason})` : "no"; lines.push(`${wf} ${step} ${role} ${prompt} ${rec} ${runs} ${asks} ${weighted} ${cost} ${lat} ${accepted} ${rework} ${cand}`); } if (report.routeSwitches && report.routeSwitches.length > 0) { lines.push(""); lines.push(`Route switches (${report.routeSwitches.length}):`); for (const item of report.routeSwitches) { const effort = item.effort ? ` effort ${item.effort}` : ""; lines.push(` - ${item.from} -> ${item.to} (${item.reason}${effort}) on ${item.stepId}`); } } if (report.candidates.length > 0) { const readiness = new Map(report.promotion.map((entry) => [entry.candidateId, entry])); lines.push(""); lines.push("Emitted Coded-Repeat Candidates:"); for (const c of report.candidates) { lines.push(` - ${c.id} [${c.kind}]: ${c.summary}`); lines.push(` Outcome: ${c.declaredOutcome}`); lines.push(` Measure: ${c.measure}`); lines.push(` Proposed diff: ${c.proposedDiffPath}`); const entry = readiness.get(c.id); if (entry) { lines.push(` Promotion (${entry.policy}): ${entry.readyForReview ? "ready for review" : "not ready for review"}; ${entry.reason}`); } } lines.push(""); lines.push("Candidates are proposals: readiness never authorizes, and activation is a reviewed Git change."); } return lines.join("\n"); } /** Review readiness of one candidate under improvement.promotionPolicy. */ export interface PromotionReadiness { policy: string; readyForReview: boolean; reason: string; } export interface PromotionAutoThreshold { minRuns?: number | undefined; minPassRate?: number | undefined; minCostSavings?: number | undefined; } /** * Report whether a candidate is ready for operator review (Decision Q11). * * No policy authorizes anything. AGENTS.md:141 forbids auto-promoting skills * or gates from telemetry, and ADR-001 (docs/contracts/architecture.md:177-180) * rejects automatic learned-policy activation: activation is a reviewed Git * change. Every policy therefore ends at an operator PR: * - manual_pr: always ready; the operator reviews the proposed diff. * - critic_quorum: ready once two distinct critic receipts are cited. * - auto_threshold: ready once the candidate's distinct runs and accepted rate * reach the thresholds and, when minCostSavings is set, recorded cost samples * show at least that mean cost; a group without cost samples is never ready. * Any other policy is not ready. */ export function evaluatePromotionPolicy( candidate: ImprovementCandidate, policy: string = "manual_pr", options: { criticReceipts?: string[] | undefined; autoThreshold?: PromotionAutoThreshold | undefined; costSamples?: number | undefined; distinctRuns?: number | undefined; } = {}, ): PromotionReadiness { if (policy === "manual_pr") { return { policy, readyForReview: true, reason: `operator PR applying ${candidate.proposedDiffPath} required` }; } if (policy === "critic_quorum") { const receipts = new Set((options.criticReceipts ?? []).map((receipt) => receipt.trim()).filter((receipt) => receipt.length > 0)); return receipts.size >= 2 ? { policy, readyForReview: true, reason: "two critic receipts cited; operator PR still required" } : { policy, readyForReview: false, reason: `awaiting 2 distinct critic receipts (have ${receipts.size})` }; } if (policy === "auto_threshold") { const minRuns = options.autoThreshold?.minRuns ?? 10; const minPassRate = options.autoThreshold?.minPassRate ?? 0.95; const minCostSavings = options.autoThreshold?.minCostSavings; const runs = options.distinctRuns ?? candidate.baselineMetrics.recurrence; const passRate = candidate.baselineMetrics.verifyPassRate; const costSamples = options.costSamples ?? 0; const meanCost = candidate.baselineMetrics.meanCost; const costMet = minCostSavings === undefined || (costSamples > 0 && meanCost >= minCostSavings); const ready = runs >= minRuns && passRate >= minPassRate && costMet; const detail = [ `runs=${runs}/${minRuns}`, `passRate=${passRate}/${minPassRate}`, ...(minCostSavings === undefined ? [] : [`meanCost=${meanCost}/${minCostSavings} over ${costSamples} cost sample(s)`]), ].join(", "); return ready ? { policy, readyForReview: true, reason: `auto-threshold met (${detail}); operator PR still required` } : { policy, readyForReview: false, reason: `auto-threshold not met (${detail})` }; } return { policy, readyForReview: false, reason: "unknown promotion policy" }; }