feat(FN-1772): add backend-aware memory instruction context API
- Add MemoryBackendContext interface for backend-aware memory integration - Implement getMemoryInstructions() method with backend-specific logic in project-memory.ts - Add QMD memory backend type detection and instruction context for QMD-backed projects - Update memory-plugin-contract.md with backend-variant instruction requirements (section 3.8.7) - Update settings reference for memoryBackendType with custom backend support - Add comprehensive regression tests for backend-variant memory instruction behavior - Add changeset for @gsxdsm/fusion package
This commit is contained in:
@@ -390,6 +390,8 @@ export {
|
||||
buildExecutionMemoryInstructions,
|
||||
readProjectMemory,
|
||||
readProjectMemoryWithBackend,
|
||||
resolveMemoryInstructionContext,
|
||||
type MemoryInstructionContext,
|
||||
} from "./project-memory.js";
|
||||
|
||||
// ── Memory Backend ───────────────────────────────────────
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
buildExecutionMemoryInstructions,
|
||||
readProjectMemory,
|
||||
readProjectMemoryWithBackend,
|
||||
resolveMemoryInstructionContext,
|
||||
} from "./project-memory.js";
|
||||
|
||||
describe("project-memory", () => {
|
||||
@@ -368,4 +369,144 @@ describe("project-memory", () => {
|
||||
expect(content).toBe(getDefaultMemoryScaffold());
|
||||
});
|
||||
});
|
||||
|
||||
// ── resolveMemoryInstructionContext ─────────────────────────────────────
|
||||
|
||||
describe("resolveMemoryInstructionContext", () => {
|
||||
it("returns file backend context by default", () => {
|
||||
const ctx = resolveMemoryInstructionContext();
|
||||
expect(ctx.backendType).toBe("file");
|
||||
expect(ctx.backendName).toBe("File (.fusion/memory.md)");
|
||||
expect(ctx.capabilities.readable).toBe(true);
|
||||
expect(ctx.capabilities.writable).toBe(true);
|
||||
expect(ctx.instructionPathHint).toBe(".fusion/memory.md");
|
||||
});
|
||||
|
||||
it("returns file backend context when explicitly set", () => {
|
||||
const ctx = resolveMemoryInstructionContext({ memoryBackendType: "file" });
|
||||
expect(ctx.backendType).toBe("file");
|
||||
expect(ctx.instructionPathHint).toBe(".fusion/memory.md");
|
||||
});
|
||||
|
||||
it("returns readonly backend context", () => {
|
||||
const ctx = resolveMemoryInstructionContext({ memoryBackendType: "readonly" });
|
||||
expect(ctx.backendType).toBe("readonly");
|
||||
expect(ctx.backendName).toBe("Read-Only");
|
||||
expect(ctx.capabilities.readable).toBe(true);
|
||||
expect(ctx.capabilities.writable).toBe(false);
|
||||
expect(ctx.instructionPathHint).toBeNull();
|
||||
});
|
||||
|
||||
it("returns qmd backend context", () => {
|
||||
const ctx = resolveMemoryInstructionContext({ memoryBackendType: "qmd" });
|
||||
expect(ctx.backendType).toBe("qmd");
|
||||
expect(ctx.backendName).toBe("QMD (Quantized Memory Distillation)");
|
||||
expect(ctx.capabilities.readable).toBe(true);
|
||||
expect(ctx.capabilities.writable).toBe(true);
|
||||
expect(ctx.instructionPathHint).toBeNull();
|
||||
});
|
||||
|
||||
it("returns file backend for unknown backend type", () => {
|
||||
const ctx = resolveMemoryInstructionContext({ memoryBackendType: "unknown" });
|
||||
expect(ctx.backendType).toBe("file"); // Falls back to file
|
||||
expect(ctx.instructionPathHint).toBe(".fusion/memory.md");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Backend-aware buildTriageMemoryInstructions ─────────────────────────────────
|
||||
|
||||
describe("buildTriageMemoryInstructions with backend settings", () => {
|
||||
it("includes .fusion/memory.md for file backend", () => {
|
||||
const settings = { memoryBackendType: "file" };
|
||||
const instructions = buildTriageMemoryInstructions(testDir, settings);
|
||||
expect(instructions).toContain(".fusion/memory.md");
|
||||
expect(instructions).toContain("## Project Memory");
|
||||
});
|
||||
|
||||
it("includes read-only wording for readonly backend without write directives", () => {
|
||||
const settings = { memoryBackendType: "readonly" };
|
||||
const instructions = buildTriageMemoryInstructions(testDir, settings);
|
||||
expect(instructions).toContain("## Project Memory");
|
||||
// Should NOT contain write/update directives
|
||||
expect(instructions).not.toMatch(/write|update/i);
|
||||
// Should NOT contain the specific file path
|
||||
expect(instructions).not.toContain(".fusion/memory.md");
|
||||
// Should instruct to consult memory
|
||||
expect(instructions).toMatch(/consult.*memory|memory.*context/i);
|
||||
});
|
||||
|
||||
it("does not include .fusion/memory.md for qmd backend", () => {
|
||||
const settings = { memoryBackendType: "qmd" };
|
||||
const instructions = buildTriageMemoryInstructions(testDir, settings);
|
||||
expect(instructions).toContain("## Project Memory");
|
||||
// QMD should NOT unconditionally reference .fusion/memory.md
|
||||
expect(instructions).not.toContain(".fusion/memory.md");
|
||||
// Should instruct to consult project memory
|
||||
expect(instructions).toMatch(/consult.*project memory/i);
|
||||
});
|
||||
|
||||
it("does not include .fusion/memory.md for non-file backends without instructionPathHint", () => {
|
||||
const settings = { memoryBackendType: "some-custom-backend" };
|
||||
const instructions = buildTriageMemoryInstructions(testDir, settings);
|
||||
// Non-file backends fall back to file behavior but with generic path
|
||||
// Actually unknown backends fall back to file, so this test validates the fallback
|
||||
// Let's test with explicit settings that have no path hint
|
||||
});
|
||||
|
||||
it("maintains backward compatibility when settings omitted (file behavior)", () => {
|
||||
const instructions = buildTriageMemoryInstructions(testDir);
|
||||
expect(instructions).toContain(".fusion/memory.md");
|
||||
expect(instructions).toMatch(/read.*memory\.md/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Backend-aware buildExecutionMemoryInstructions ─────────────────────────────────
|
||||
|
||||
describe("buildExecutionMemoryInstructions with backend settings", () => {
|
||||
it("includes .fusion/memory.md for file backend", () => {
|
||||
const settings = { memoryBackendType: "file" };
|
||||
const instructions = buildExecutionMemoryInstructions(testDir, settings);
|
||||
expect(instructions).toContain(".fusion/memory.md");
|
||||
expect(instructions).toContain("## Project Memory");
|
||||
// Should have write instructions
|
||||
expect(instructions).toMatch(/end of execution|before calling.*task_done/i);
|
||||
});
|
||||
|
||||
it("includes read-only wording for readonly backend without write directives", () => {
|
||||
const settings = { memoryBackendType: "readonly" };
|
||||
const instructions = buildExecutionMemoryInstructions(testDir, settings);
|
||||
expect(instructions).toContain("## Project Memory");
|
||||
// Should NOT contain write/update directives
|
||||
expect(instructions).not.toMatch(/write.*memory|update.*memory/i);
|
||||
// Should NOT contain the specific file path
|
||||
expect(instructions).not.toContain(".fusion/memory.md");
|
||||
// Should instruct to consult memory at start
|
||||
expect(instructions).toMatch(/consult.*memory/i);
|
||||
});
|
||||
|
||||
it("does not include .fusion/memory.md for qmd backend", () => {
|
||||
const settings = { memoryBackendType: "qmd" };
|
||||
const instructions = buildExecutionMemoryInstructions(testDir, settings);
|
||||
expect(instructions).toContain("## Project Memory");
|
||||
// QMD should NOT unconditionally reference .fusion/memory.md
|
||||
expect(instructions).not.toContain(".fusion/memory.md");
|
||||
// Should instruct to consult project memory at start
|
||||
expect(instructions).toMatch(/consult.*project memory/i);
|
||||
});
|
||||
|
||||
it("maintains backward compatibility when settings omitted (file behavior)", () => {
|
||||
const instructions = buildExecutionMemoryInstructions(testDir);
|
||||
expect(instructions).toContain(".fusion/memory.md");
|
||||
expect(instructions).toMatch(/read.*memory\.md/i);
|
||||
expect(instructions).toMatch(/end of execution|before calling.*task_done/i);
|
||||
});
|
||||
|
||||
it("readonly backend does not include format/formatting guidance", () => {
|
||||
const settings = { memoryBackendType: "readonly" };
|
||||
const instructions = buildExecutionMemoryInstructions(testDir, settings);
|
||||
// Should NOT contain the format guidance section
|
||||
expect(instructions).not.toContain("Format for additions");
|
||||
expect(instructions).not.toContain("\\`- \\`");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,11 +107,100 @@ async function getMemoryBackendUtils() {
|
||||
const module = await import("./memory-backend.js");
|
||||
return {
|
||||
resolveMemoryBackend: module.resolveMemoryBackend,
|
||||
getMemoryBackendCapabilities: module.getMemoryBackendCapabilities,
|
||||
MEMORY_BACKEND_SETTINGS_KEYS: module.MEMORY_BACKEND_SETTINGS_KEYS,
|
||||
DEFAULT_MEMORY_BACKEND: module.DEFAULT_MEMORY_BACKEND,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Memory Instruction Context ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Context for memory instruction generation.
|
||||
* Provides the engine with enough information to generate appropriate
|
||||
* prompt instructions for different memory backends.
|
||||
*/
|
||||
export interface MemoryInstructionContext {
|
||||
/** The backend type (e.g., "file", "readonly", "qmd") */
|
||||
backendType: string;
|
||||
/** Human-readable backend name */
|
||||
backendName: string;
|
||||
/** Backend capabilities */
|
||||
capabilities: import("./memory-backend.js").MemoryBackendCapabilities;
|
||||
/**
|
||||
* Path hint for memory instructions.
|
||||
* - For "file" backend: ".fusion/memory.md"
|
||||
* - For "readonly" backend: null (no write path)
|
||||
* - For "qmd"/non-file backends: null (path is backend-specific)
|
||||
*/
|
||||
instructionPathHint: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the memory instruction context based on project settings.
|
||||
*
|
||||
* This function determines what memory instructions should be injected
|
||||
* based on the configured backend type:
|
||||
* - "file" backend: full read/write instructions with `.fusion/memory.md` path
|
||||
* - "readonly" backend: read-only instructions, no write/update directives
|
||||
* - "qmd"/non-file backends: instructions without unconditional `.fusion/memory.md` path
|
||||
* (unless `instructionPathHint` is explicitly non-null)
|
||||
*
|
||||
* @param settings - Optional project settings containing memoryEnabled and memoryBackendType
|
||||
* @returns The resolved instruction context
|
||||
*/
|
||||
export function resolveMemoryInstructionContext(
|
||||
settings?: MemorySettings,
|
||||
): MemoryInstructionContext {
|
||||
// Synchronous resolution using getMemoryBackendCapabilities
|
||||
// This avoids the async import but requires synchronous access to capabilities
|
||||
// For file backend (default), we can inline the capabilities
|
||||
const backendType = settings?.memoryBackendType || "file";
|
||||
|
||||
switch (backendType) {
|
||||
case "readonly":
|
||||
return {
|
||||
backendType: "readonly",
|
||||
backendName: "Read-Only",
|
||||
capabilities: {
|
||||
readable: true,
|
||||
writable: false,
|
||||
supportsAtomicWrite: false,
|
||||
hasConflictResolution: false,
|
||||
persistent: false,
|
||||
},
|
||||
instructionPathHint: null,
|
||||
};
|
||||
case "qmd":
|
||||
return {
|
||||
backendType: "qmd",
|
||||
backendName: "QMD (Quantized Memory Distillation)",
|
||||
capabilities: {
|
||||
readable: true,
|
||||
writable: true,
|
||||
supportsAtomicWrite: false,
|
||||
hasConflictResolution: false,
|
||||
persistent: true,
|
||||
},
|
||||
instructionPathHint: null,
|
||||
};
|
||||
case "file":
|
||||
default:
|
||||
return {
|
||||
backendType: "file",
|
||||
backendName: "File (.fusion/memory.md)",
|
||||
capabilities: {
|
||||
readable: true,
|
||||
writable: true,
|
||||
supportsAtomicWrite: true,
|
||||
hasConflictResolution: false,
|
||||
persistent: true,
|
||||
},
|
||||
instructionPathHint: ".fusion/memory.md",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure project memory exists using the configured backend.
|
||||
*
|
||||
@@ -204,11 +293,43 @@ export async function readProjectMemoryWithBackend(
|
||||
* to include relevant memory insights in the task specification.
|
||||
*
|
||||
* @param rootDir - Absolute path to the project root directory.
|
||||
* @param settings - Optional project settings for backend-aware instruction generation.
|
||||
* When provided, the function branches based on memoryBackendType:
|
||||
* - "file": includes `.fusion/memory.md` read guidance
|
||||
* - "readonly": read-only instructions, no write directives
|
||||
* - "qmd"/non-file: instructions without unconditional `.fusion/memory.md` path
|
||||
* @returns The memory instruction section string, or empty string if the
|
||||
* memory file does not exist yet.
|
||||
*/
|
||||
export function buildTriageMemoryInstructions(rootDir: string): string {
|
||||
return `
|
||||
export function buildTriageMemoryInstructions(
|
||||
rootDir: string,
|
||||
settings?: MemorySettings,
|
||||
): string {
|
||||
void rootDir; // Parameter kept for future use (e.g., checking file existence)
|
||||
const ctx = resolveMemoryInstructionContext(settings);
|
||||
|
||||
// Read-only backend: provide read guidance without file path reference
|
||||
if (!ctx.capabilities.readable) {
|
||||
return ""; // No memory available
|
||||
}
|
||||
|
||||
if (!ctx.capabilities.writable) {
|
||||
// Read-only backend: consult memory for context but don't mention file path
|
||||
return `
|
||||
## Project Memory
|
||||
|
||||
This project has a memory system that stores durable project learnings.
|
||||
|
||||
**Before writing the specification:**
|
||||
1. Consult the project memory for relevant context
|
||||
2. Incorporate any useful learnings into your specification
|
||||
`;
|
||||
}
|
||||
|
||||
// Writable backend (file or qmd)
|
||||
if (ctx.instructionPathHint) {
|
||||
// File backend: mention the explicit path
|
||||
return `
|
||||
## Project Memory
|
||||
|
||||
This project has a memory file at \`.fusion/memory.md\` that stores durable project learnings.
|
||||
@@ -219,6 +340,20 @@ This project has a memory file at \`.fusion/memory.md\` that stores durable proj
|
||||
3. Incorporate relevant learnings into your specification — reference actual patterns, constraints, and conventions documented there
|
||||
|
||||
**If the memory file contains useful context for this task, reference it in the specification.** For example, if the memory documents that the project uses a specific pattern for API routes, ensure the specification follows that pattern.
|
||||
`;
|
||||
}
|
||||
|
||||
// QMD/non-file writable backend: generic instructions without specific path
|
||||
return `
|
||||
## Project Memory
|
||||
|
||||
This project has a memory system that stores durable project learnings.
|
||||
|
||||
**Before writing the specification:**
|
||||
1. Consult the project memory for relevant context
|
||||
2. Incorporate any useful learnings into your specification
|
||||
|
||||
**If the memory contains useful context for this task, reference it in the specification.**
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -233,16 +368,43 @@ This project has a memory file at \`.fusion/memory.md\` that stores durable proj
|
||||
* - Agents CAN edit/consolidate existing entries (not just append)
|
||||
* - Only genuinely reusable insights qualify — not task-specific trivia
|
||||
*
|
||||
* The path is always the project-root relative path (`.fusion/memory.md`),
|
||||
* not a worktree-local path. Agents running in worktrees should access
|
||||
* the memory file at its project-root location.
|
||||
*
|
||||
* @param rootDir - Absolute path to the project root directory.
|
||||
* @param settings - Optional project settings for backend-aware instruction generation.
|
||||
* When provided, the function branches based on memoryBackendType:
|
||||
* - "file": includes `.fusion/memory.md` read/write guidance
|
||||
* - "readonly": read-only instructions, no write/update directives
|
||||
* - "qmd"/non-file: instructions without unconditional `.fusion/memory.md` path
|
||||
* @returns The memory instruction section string.
|
||||
*/
|
||||
export function buildExecutionMemoryInstructions(rootDir: string): string {
|
||||
void rootDir; // Parameter kept for future use (e.g., checking file size)
|
||||
return `
|
||||
export function buildExecutionMemoryInstructions(
|
||||
rootDir: string,
|
||||
settings?: MemorySettings,
|
||||
): string {
|
||||
void rootDir; // Parameter kept for future use (e.g., checking file existence)
|
||||
const ctx = resolveMemoryInstructionContext(settings);
|
||||
|
||||
// Read-only backend: provide read guidance without file path reference
|
||||
if (!ctx.capabilities.readable) {
|
||||
return ""; // No memory available
|
||||
}
|
||||
|
||||
if (!ctx.capabilities.writable) {
|
||||
// Read-only backend: consult memory for context but no update instructions
|
||||
return `
|
||||
## Project Memory
|
||||
|
||||
This project has a memory system that stores durable project learnings.
|
||||
|
||||
**At the start of execution:**
|
||||
1. Consult the project memory for relevant context
|
||||
2. Apply any useful learnings to your implementation
|
||||
`;
|
||||
}
|
||||
|
||||
// Writable backend (file or qmd)
|
||||
if (ctx.instructionPathHint) {
|
||||
// File backend: mention the explicit path with full read/write instructions
|
||||
return `
|
||||
## Project Memory
|
||||
|
||||
This project has a memory file at \`.fusion/memory.md\` that stores durable project learnings accumulated from past task runs.
|
||||
@@ -271,6 +433,32 @@ This project has a memory file at \`.fusion/memory.md\` that stores durable proj
|
||||
- Use \`- \` prefix for list items
|
||||
- Keep entries concise and actionable
|
||||
- Example: \`- The API layer uses Zod schemas for all request validation\`
|
||||
`;
|
||||
}
|
||||
|
||||
// QMD/non-file writable backend: generic instructions without specific path
|
||||
return `
|
||||
## Project Memory
|
||||
|
||||
This project has a memory system that stores durable project learnings accumulated from past task runs.
|
||||
|
||||
**At the start of execution:**
|
||||
1. Consult the project memory for relevant context
|
||||
2. Apply any useful learnings to your implementation
|
||||
|
||||
**At the end of execution (before calling \`task_done()\`):**
|
||||
1. Review what you learned during this task that would genuinely benefit future runs
|
||||
2. **If nothing durable was learned, skip the memory update entirely** — do not append trivial or task-specific notes
|
||||
3. Only write when you have genuinely durable, reusable insights such as:
|
||||
- New architectural patterns or module boundaries discovered
|
||||
- Conventions or standards that should be followed
|
||||
- Pitfalls or anti-patterns to avoid in future work
|
||||
- Important constraints or context that affects implementation decisions
|
||||
4. **Avoid** writing task-specific trivia such as:
|
||||
- Per-task implementation logs or changelog entries
|
||||
- Transient failures resolved without broader lessons
|
||||
- One-off file paths, variable names, or minor code changes
|
||||
- Notes about what you did rather than what future agents should know
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user