feat: add feedback memory primitive

This commit is contained in:
artale 2026-06-18 03:46:09 +02:00
parent df845b63cc
commit 779b8d3eca
5 changed files with 91 additions and 1 deletions

View File

@ -260,6 +260,10 @@ fable-agent plinius godmode "improve explanation quality"
- `--add-rule <text>` - `--add-rule <text>`
- `--add-failure <text>` - `--add-failure <text>`
- `--add-lesson <text>` - `--add-lesson <text>`
- `--approved <text>`
- `--rejected <text>`
- `--performance <text>`
- `--repo <path>`
- `--import` - `--import`
- `fable5 workflow <pattern> <task>` - `fable5 workflow <pattern> <task>`
- `-s, --subtasks <list>` - `-s, --subtasks <list>`

View File

@ -0,0 +1,36 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { appendFeedbackMemory, feedbackMemoryPath, feedbackMemoryStats } from "./feedback-memory.js";
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
function tmpRoot(): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "fable-feedback-memory-"));
roots.push(root);
return root;
}
describe("feedback memory", () => {
it("appends feedback to the right markdown file", () => {
const root = tmpRoot();
const entry = appendFeedbackMemory(root, "approved", "ship this\nnow", new Date("2026-06-18T00:00:00.000Z"));
expect(entry.type).toBe("approved");
expect(fs.readFileSync(feedbackMemoryPath(root, "approved"), "utf-8")).toContain("ship this now");
});
it("counts entries per memory file", () => {
const root = tmpRoot();
appendFeedbackMemory(root, "approved", "ok");
appendFeedbackMemory(root, "rejected", "bad");
appendFeedbackMemory(root, "rejected", "worse");
expect(feedbackMemoryStats(root)).toEqual({ approved: 1, rejected: 2, performance: 0 });
});
});

View File

@ -0,0 +1,37 @@
import * as fs from "node:fs";
import * as path from "node:path";
export type FeedbackMemoryType = "approved" | "rejected" | "performance";
export interface FeedbackMemoryEntry {
type: FeedbackMemoryType;
text: string;
createdAt: string;
}
const FILES: Record<FeedbackMemoryType, string> = {
approved: "approved.md",
rejected: "rejected.md",
performance: "performance.md",
};
export function feedbackMemoryPath(repo: string, type: FeedbackMemoryType): string {
return path.join(repo, ".fable", "memory", FILES[type]);
}
export function appendFeedbackMemory(repo: string, type: FeedbackMemoryType, text: string, now = new Date()): FeedbackMemoryEntry {
const file = feedbackMemoryPath(repo, type);
const entry = { type, text, createdAt: now.toISOString() };
fs.mkdirSync(path.dirname(file), { recursive: true });
if (!fs.existsSync(file)) fs.writeFileSync(file, `# ${type}\n\n`);
fs.appendFileSync(file, `- ${entry.createdAt}: ${text.replace(/\r?\n/g, " ")}\n`);
return entry;
}
export function feedbackMemoryStats(repo: string): Record<FeedbackMemoryType, number> {
return Object.fromEntries((Object.keys(FILES) as FeedbackMemoryType[]).map((type) => {
const file = feedbackMemoryPath(repo, type);
const count = fs.existsSync(file) ? fs.readFileSync(file, "utf-8").split(/\r?\n/).filter((line) => line.startsWith("- ")).length : 0;
return [type, count];
})) as Record<FeedbackMemoryType, number>;
}

View File

@ -65,6 +65,9 @@ export type { MissionHeartbeatOptions, MissionHeartbeatReceipt, MissionHeartbeat
export { createEvalTraceReceipt, writeEvalTraceReceipt } from "./eval-trace.js"; export { createEvalTraceReceipt, writeEvalTraceReceipt } from "./eval-trace.js";
export type { EvalTraceCommand, EvalTraceReceipt } from "./eval-trace.js"; export type { EvalTraceCommand, EvalTraceReceipt } from "./eval-trace.js";
export { appendFeedbackMemory, feedbackMemoryPath, feedbackMemoryStats } from "./feedback-memory.js";
export type { FeedbackMemoryEntry, FeedbackMemoryType } from "./feedback-memory.js";
export { runCyberPreflight, writeCyberPreflightReceipt } from "./cyber-preflight.js"; export { runCyberPreflight, writeCyberPreflightReceipt } from "./cyber-preflight.js";
export type { CyberPreflightCheck, CyberPreflightOptions, CyberPreflightReceipt } from "./cyber-preflight.js"; export type { CyberPreflightCheck, CyberPreflightOptions, CyberPreflightReceipt } from "./cyber-preflight.js";

View File

@ -1940,12 +1940,22 @@ fable
.option("--add-rule <text>", "Add a general rule") .option("--add-rule <text>", "Add a general rule")
.option("--add-failure <text>", "Add a failure entry") .option("--add-failure <text>", "Add a failure entry")
.option("--add-lesson <text>", "Add a lesson learned") .option("--add-lesson <text>", "Add a lesson learned")
.option("--approved <text>", "Append approved feedback memory")
.option("--rejected <text>", "Append rejected feedback memory")
.option("--performance <text>", "Append performance feedback memory")
.option("--repo <path>", "Repo path for .fable/memory", ".")
.option("--import", "Import from knowledge base") .option("--import", "Import from knowledge base")
.action(async (project: string, opts: Record<string, unknown>) => { .action(async (project: string, opts: Record<string, unknown>) => {
const { FiveStageStateFile } = await import("./fable5/state-file-5stage.js"); const { FiveStageStateFile } = await import("./fable5/state-file-5stage.js");
const state = new FiveStageStateFile(); const state = new FiveStageStateFile();
if (opts.addFact) { if (opts.approved || opts.rejected || opts.performance) {
const { appendFeedbackMemory } = await import("./fable5/feedback-memory.js");
const type = opts.approved ? "approved" : opts.rejected ? "rejected" : "performance";
const text = (opts.approved ?? opts.rejected ?? opts.performance) as string;
appendFeedbackMemory(path.resolve(opts.repo as string), type, `[${project}] ${text}`);
console.log(`${type} feedback memory appended`);
} else if (opts.addFact) {
state.addVerifiedFact(project, opts.addFact as string, "cli"); state.addVerifiedFact(project, opts.addFact as string, "cli");
console.log(` ✓ Verified fact added to "${project}"`); console.log(` ✓ Verified fact added to "${project}"`);
} else if (opts.addRule) { } else if (opts.addRule) {