feat: add feedback memory primitive
This commit is contained in:
parent
df845b63cc
commit
779b8d3eca
|
|
@ -260,6 +260,10 @@ fable-agent plinius godmode "improve explanation quality"
|
|||
- `--add-rule <text>`
|
||||
- `--add-failure <text>`
|
||||
- `--add-lesson <text>`
|
||||
- `--approved <text>`
|
||||
- `--rejected <text>`
|
||||
- `--performance <text>`
|
||||
- `--repo <path>`
|
||||
- `--import`
|
||||
- `fable5 workflow <pattern> <task>`
|
||||
- `-s, --subtasks <list>`
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -65,6 +65,9 @@ export type { MissionHeartbeatOptions, MissionHeartbeatReceipt, MissionHeartbeat
|
|||
export { createEvalTraceReceipt, writeEvalTraceReceipt } 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 type { CyberPreflightCheck, CyberPreflightOptions, CyberPreflightReceipt } from "./cyber-preflight.js";
|
||||
|
||||
|
|
|
|||
12
src/index.ts
12
src/index.ts
|
|
@ -1940,12 +1940,22 @@ fable
|
|||
.option("--add-rule <text>", "Add a general rule")
|
||||
.option("--add-failure <text>", "Add a failure entry")
|
||||
.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")
|
||||
.action(async (project: string, opts: Record<string, unknown>) => {
|
||||
const { FiveStageStateFile } = await import("./fable5/state-file-5stage.js");
|
||||
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");
|
||||
console.log(` ✓ Verified fact added to "${project}"`);
|
||||
} else if (opts.addRule) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue