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:
gsxdsm
2026-04-13 22:03:42 -07:00
parent ce518f3b61
commit f6ae0e2e82
9 changed files with 502 additions and 18 deletions

View File

@@ -390,6 +390,8 @@ export {
buildExecutionMemoryInstructions,
readProjectMemory,
readProjectMemoryWithBackend,
resolveMemoryInstructionContext,
type MemoryInstructionContext,
} from "./project-memory.js";
// ── Memory Backend ───────────────────────────────────────

View File

@@ -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("\\`- \\`");
});
});
});

View File

@@ -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
`;
}