feat(FN-4455): complete Step 5 — migrate memory mode resolution and tests
Fusion-Task-Id: FN-4455 Fusion-Task-Lineage: 735091fa-9d42-4f23-9c5b-3e5889da99a6
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Agent, GlobalSettings, ProjectSettings } from "../types.js";
|
||||
import type { Agent, GlobalSettings } from "../types.js";
|
||||
import { resolveAgentMemoryInclusionMode } from "../agent-memory-mode.js";
|
||||
|
||||
function makeAgent(mode?: unknown): Agent {
|
||||
@@ -16,24 +16,14 @@ function makeAgent(mode?: unknown): Agent {
|
||||
}
|
||||
|
||||
describe("resolveAgentMemoryInclusionMode", () => {
|
||||
it("prefers per-agent override over project and global", () => {
|
||||
it("prefers per-agent override over global", () => {
|
||||
const result = resolveAgentMemoryInclusionMode({
|
||||
agent: makeAgent("off"),
|
||||
projectSettings: { agentMemoryInclusionMode: "index" } as ProjectSettings,
|
||||
globalSettings: { agentMemoryInclusionMode: "full" } as GlobalSettings,
|
||||
});
|
||||
expect(result).toEqual({ mode: "off", source: "agent" });
|
||||
});
|
||||
|
||||
it("prefers project over global", () => {
|
||||
const result = resolveAgentMemoryInclusionMode({
|
||||
agent: makeAgent(),
|
||||
projectSettings: { agentMemoryInclusionMode: "index" } as ProjectSettings,
|
||||
globalSettings: { agentMemoryInclusionMode: "off" } as GlobalSettings,
|
||||
});
|
||||
expect(result).toEqual({ mode: "index", source: "project" });
|
||||
});
|
||||
|
||||
it("prefers global over default", () => {
|
||||
const result = resolveAgentMemoryInclusionMode({
|
||||
agent: makeAgent(),
|
||||
@@ -50,7 +40,6 @@ describe("resolveAgentMemoryInclusionMode", () => {
|
||||
it("ignores invalid values and falls through", () => {
|
||||
const result = resolveAgentMemoryInclusionMode({
|
||||
agent: makeAgent("bad"),
|
||||
projectSettings: { agentMemoryInclusionMode: "nope" as never } as ProjectSettings,
|
||||
globalSettings: { agentMemoryInclusionMode: "index" } as GlobalSettings,
|
||||
});
|
||||
expect(result).toEqual({ mode: "index", source: "global" });
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("settings key parity", () => {
|
||||
expect(isGlobalOnlySettingsKey("persistAgentThinkingLogEphemeral")).toBe(true);
|
||||
expect(isGlobalSettingsKey("researchSettings")).toBe(false);
|
||||
expect(isGlobalSettingsKey("agentMemoryInclusionMode")).toBe(true);
|
||||
expect(isProjectSettingsKey("agentMemoryInclusionMode")).toBe(true);
|
||||
expect(isProjectSettingsKey("agentMemoryInclusionMode")).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults persisted thinking logs to disabled", () => {
|
||||
@@ -190,7 +190,7 @@ describe("settings key parity", () => {
|
||||
it("only intentional shared keys appear in both global and project scopes", () => {
|
||||
const projectKeySet = new Set(PROJECT_SETTINGS_KEYS as readonly string[]);
|
||||
const overlap = (GLOBAL_SETTINGS_KEYS as readonly string[]).filter((key) => projectKeySet.has(key));
|
||||
expect(overlap).toEqual(["taskTokenBudget", "githubTrackingDefaultRepo", "agentMemoryInclusionMode"]);
|
||||
expect(overlap).toEqual(["taskTokenBudget", "githubTrackingDefaultRepo"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { Agent, AgentMemoryInclusionMode, GlobalSettings, ProjectSettings } from "./types.js";
|
||||
import type { Agent, AgentMemoryInclusionMode, GlobalSettings } from "./types.js";
|
||||
|
||||
export type AgentMemoryInclusionModeSource = "agent" | "project" | "global" | "default";
|
||||
export type AgentMemoryInclusionModeSource = "agent" | "global" | "default";
|
||||
|
||||
export interface ResolveAgentMemoryInclusionModeInput {
|
||||
agent?: Agent | null;
|
||||
projectSettings?: ProjectSettings | null;
|
||||
globalSettings?: GlobalSettings | null;
|
||||
}
|
||||
|
||||
@@ -19,7 +18,6 @@ function isAgentMemoryInclusionMode(value: unknown): value is AgentMemoryInclusi
|
||||
|
||||
export function resolveAgentMemoryInclusionMode({
|
||||
agent,
|
||||
projectSettings,
|
||||
globalSettings,
|
||||
}: ResolveAgentMemoryInclusionModeInput): ResolvedAgentMemoryInclusionMode {
|
||||
const agentMode = agent?.runtimeConfig && typeof agent.runtimeConfig === "object"
|
||||
@@ -29,11 +27,6 @@ export function resolveAgentMemoryInclusionMode({
|
||||
return { mode: agentMode, source: "agent" };
|
||||
}
|
||||
|
||||
const projectMode = projectSettings?.agentMemoryInclusionMode;
|
||||
if (isAgentMemoryInclusionMode(projectMode)) {
|
||||
return { mode: projectMode, source: "project" };
|
||||
}
|
||||
|
||||
const globalMode = globalSettings?.agentMemoryInclusionMode;
|
||||
if (isAgentMemoryInclusionMode(globalMode)) {
|
||||
return { mode: globalMode, source: "global" };
|
||||
|
||||
@@ -94,7 +94,6 @@ export async function ensureMemoryFile(rootDir: string): Promise<boolean> {
|
||||
type MemorySettings = {
|
||||
memoryEnabled?: boolean;
|
||||
memoryBackendType?: string;
|
||||
agentMemoryInclusionMode?: "full" | "index" | "off";
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
|
||||
@@ -298,7 +298,6 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
taskEvaluationFollowUpPolicy: "off",
|
||||
taskEvaluationRetention: undefined,
|
||||
memoryEnabled: true,
|
||||
agentMemoryInclusionMode: undefined,
|
||||
memoryBackendType: "qmd",
|
||||
memoryAutoSummarizeEnabled: false,
|
||||
memoryAutoSummarizeThresholdChars: 50_000,
|
||||
|
||||
@@ -2657,12 +2657,6 @@ export interface ProjectSettings {
|
||||
* in their prompts and will not read or write to .fusion/memory/ files.
|
||||
* Default: true (enabled for backward compatibility). */
|
||||
memoryEnabled?: boolean;
|
||||
/** Agent memory prompt inclusion mode for this project.
|
||||
* - "full": inline full curated memory content into prompts
|
||||
* - "index": include only a compact memory index and fetch details via tools
|
||||
* - "off": omit agent-memory prompt sections entirely
|
||||
*/
|
||||
agentMemoryInclusionMode?: AgentMemoryInclusionMode;
|
||||
/** Memory backend type for pluggable memory storage.
|
||||
* Available built-in backends:
|
||||
* - "qmd": QMD (Quantized Memory Distillation) backend using the qmd CLI tool (default)
|
||||
|
||||
@@ -55,7 +55,7 @@ async function createHarness(mode: "full" | "index" | "off"): Promise<Harness> {
|
||||
const globalDir = mkdtempSync(join(tmpdir(), "hb-memory-mode-global-"));
|
||||
const taskStore = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
|
||||
await taskStore.init();
|
||||
await taskStore.updateSettings({ agentMemoryInclusionMode: mode });
|
||||
await taskStore.updateGlobalSettings({ agentMemoryInclusionMode: mode });
|
||||
const agentStore = new AgentStore({ rootDir: taskStore.getFusionDir(), taskStore, inMemoryDb: true });
|
||||
const agent = await agentStore.createAgent({
|
||||
name: "Memory Mode Agent",
|
||||
@@ -125,6 +125,7 @@ describe("heartbeat memory inclusion mode", () => {
|
||||
|
||||
const transitionLogs = appendSpy.mock.calls.filter(([, , entry]) => entry.text.includes("Agent memory inclusion mode:"));
|
||||
expect(transitionLogs).toHaveLength(1);
|
||||
expect(transitionLogs[0]?.[2].taskId).toBe("heartbeat");
|
||||
|
||||
await harness.agentStore.updateAgent(harness.agentId, {
|
||||
runtimeConfig: { enabled: true, agentMemoryInclusionMode: "off", lastAgentMemoryInclusionMode: "index" },
|
||||
|
||||
@@ -2089,7 +2089,7 @@ export class HeartbeatMonitor {
|
||||
|
||||
const resolvedMemoryMode = resolveAgentMemoryInclusionMode({
|
||||
agent,
|
||||
projectSettings: memorySettings,
|
||||
globalSettings: memorySettings,
|
||||
});
|
||||
const priorMemoryMode = agent.runtimeConfig?.lastAgentMemoryInclusionMode;
|
||||
const baseHeartbeatSystemPrompt = adjustHeartbeatMemoryPrimer(
|
||||
@@ -2157,13 +2157,10 @@ export class HeartbeatMonitor {
|
||||
|
||||
if (priorMemoryMode !== resolvedMemoryMode.mode) {
|
||||
const from = priorMemoryMode ? priorMemoryMode : "(initial)";
|
||||
const runContextTaskId = typeof run.contextSnapshot?.taskId === "string"
|
||||
? run.contextSnapshot.taskId
|
||||
: undefined;
|
||||
try {
|
||||
await this.store.appendRunLog(agentId, run.id, {
|
||||
timestamp: new Date().toISOString(),
|
||||
taskId: taskId ?? runContextTaskId ?? "heartbeat",
|
||||
taskId: taskId ?? "heartbeat",
|
||||
type: "text",
|
||||
text: `Agent memory inclusion mode: ${from} → ${resolvedMemoryMode.mode} (source: ${resolvedMemoryMode.source})`,
|
||||
});
|
||||
|
||||
@@ -2425,12 +2425,12 @@ export class TaskExecutor {
|
||||
if (agent.instructionsText || agent.instructionsPath) {
|
||||
try {
|
||||
const ratingSummary = await this.options.agentStore.getRatingSummary(agent.id);
|
||||
const mode = resolveAgentMemoryInclusionMode({ agent, projectSettings: settings }).mode;
|
||||
const mode = resolveAgentMemoryInclusionMode({ agent, globalSettings: settings }).mode;
|
||||
return await resolveAgentInstructions(agent, this.rootDir, ratingSummary, mode);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
executorLog.warn(`${agent.id}: failed to load rating summary for instruction resolution, falling back to default instructions: ${msg}`);
|
||||
const mode = resolveAgentMemoryInclusionMode({ agent, projectSettings: settings }).mode;
|
||||
const mode = resolveAgentMemoryInclusionMode({ agent, globalSettings: settings }).mode;
|
||||
return await resolveAgentInstructions(agent, this.rootDir, undefined, mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,7 +387,7 @@ export async function reviewStep(
|
||||
const agents = await options.agentStore.listAgents({ role: "reviewer" });
|
||||
for (const agent of agents) {
|
||||
if (agent.instructionsText || agent.instructionsPath) {
|
||||
const memoryMode = resolveAgentMemoryInclusionMode({ agent, projectSettings: options.settings }).mode;
|
||||
const memoryMode = resolveAgentMemoryInclusionMode({ agent, globalSettings: options.settings }).mode;
|
||||
reviewerInstructions = await resolveAgentInstructions(agent, options.rootDir, undefined, memoryMode);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1039,7 +1039,7 @@ export class TriageProcessor {
|
||||
// Resolve per-agent custom instructions for the triage role or assigned agent.
|
||||
let triageInstructions = "";
|
||||
if (assignedAgent) {
|
||||
const memoryMode = resolveAgentMemoryInclusionMode({ agent: assignedAgent, projectSettings: settings }).mode;
|
||||
const memoryMode = resolveAgentMemoryInclusionMode({ agent: assignedAgent, globalSettings: settings }).mode;
|
||||
triageInstructions = await resolveAgentInstructionsWithRatings(
|
||||
assignedAgent,
|
||||
this.rootDir,
|
||||
@@ -1052,7 +1052,7 @@ export class TriageProcessor {
|
||||
for (const agent of agents) {
|
||||
triageRuntimeHint ??= extractRuntimeHint(agent.runtimeConfig);
|
||||
if (agent.instructionsText || agent.instructionsPath || agent.soul || agent.memory) {
|
||||
const memoryMode = resolveAgentMemoryInclusionMode({ agent, projectSettings: settings }).mode;
|
||||
const memoryMode = resolveAgentMemoryInclusionMode({ agent, globalSettings: settings }).mode;
|
||||
triageInstructions = await resolveAgentInstructions(agent, this.rootDir, undefined, memoryMode);
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user