feat(FN-3718): add agent workspace memory reader and include workspace memo
Merged branch lands two features: FN-3718 wires workspace memory into the agent instruction pipeline — adding a reader helper, injecting workspace memory into agent instructions and identity snapshots, and documenting the resolution order — and FN-3716 adds planning mode priority controls for task r Fusion-Task-Id: FN-3718
This commit is contained in:
8
.changeset/FN-3718-agent-memory-lookup.md
Normal file
8
.changeset/FN-3718-agent-memory-lookup.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix agent memory lookup: the system prompt's "## Agent Memory" section and the
|
||||
heartbeat Identity Snapshot now read from the on-disk agent-memory workspace
|
||||
(`.fusion/agent-memory/{agentId}/MEMORY.md`) when the inline `agent.memory`
|
||||
field is empty, matching the documented contract.
|
||||
@@ -285,7 +285,11 @@ Runtime behavior:
|
||||
- `.fusion/agent-memory/{agentId}/DREAMS.md` (synthesized patterns)
|
||||
- `.fusion/agent-memory/{agentId}/YYYY-MM-DD.md` (daily notes)
|
||||
- `fn_memory_get` is intentionally bounded to those same files only.
|
||||
- Empty inline `agent.memory` does **not** disable search/read of existing dreams/daily files once the agent-memory workspace exists.
|
||||
- Agent memory resolution order is:
|
||||
1. Inline `agent.memory` (highest priority)
|
||||
2. `.fusion/agent-memory/{agentId}/MEMORY.md` (fallback when inline is empty, and supplemental long-term section when inline is present)
|
||||
3. Additional `.fusion/agent-memory/{agentId}/DREAMS.md` and daily files surfaced via `fn_memory_search`/`fn_memory_get`
|
||||
- Empty inline `agent.memory` does **not** disable search/read of existing workspace files once the agent-memory workspace exists.
|
||||
|
||||
This layered behavior is shared by heartbeat agents and task-scoped sessions that inherit agent identity.
|
||||
|
||||
@@ -648,10 +652,10 @@ Heartbeat runs are composed from multiple prompt layers so each wake has full id
|
||||
- Inline instructions (`instructionsText`)
|
||||
- File-backed instructions (`instructionsPath`)
|
||||
- Soul/personality (`soul`)
|
||||
- Agent memory (`memory`)
|
||||
- Agent memory resolved from inline `agent.memory` first, then `.fusion/agent-memory/{agentId}/MEMORY.md` as fallback/supplement
|
||||
- Optional project memory guidance (when memory is enabled)
|
||||
3. **Execution prompt framing**
|
||||
- `Identity Snapshot` block (agent ID/role + loaded soul/instructions/memory preview)
|
||||
- `Identity Snapshot` block (agent ID/role + loaded soul/instructions/memory preview; `memory: loaded` when either inline memory or workspace `MEMORY.md` is present)
|
||||
- `Wake Delta` block (source, trigger detail, wake reason, assignment/comments/messages)
|
||||
- Heartbeat procedure block (task-scoped or no-task variant, plus optional per-agent procedure override file)
|
||||
|
||||
|
||||
@@ -103,6 +103,60 @@ describe("resolveAgentInstructions", () => {
|
||||
expect(result).not.toContain("## Agent Memory");
|
||||
});
|
||||
|
||||
it("uses workspace MEMORY.md when inline memory is empty", async () => {
|
||||
await mkdir(join(testDir, ".fusion", "agent-memory", "agent-test"), { recursive: true });
|
||||
await writeFile(
|
||||
join(testDir, ".fusion", "agent-memory", "agent-test", "MEMORY.md"),
|
||||
"\nworkspace memory content\n",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = await resolveAgentInstructions(makeAgent({ memory: "" }), testDir);
|
||||
expect(result).toContain("## Agent Memory");
|
||||
expect(result).toContain("workspace memory content");
|
||||
expect(result).toContain("_Source: .fusion/agent-memory/agent-test/MEMORY.md_");
|
||||
});
|
||||
|
||||
it("renders both inline and workspace memory when both exist", async () => {
|
||||
await mkdir(join(testDir, ".fusion", "agent-memory", "agent-test"), { recursive: true });
|
||||
await writeFile(
|
||||
join(testDir, ".fusion", "agent-memory", "agent-test", "MEMORY.md"),
|
||||
"workspace memory content",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = await resolveAgentInstructions(makeAgent({ memory: "inline memory content" }), testDir);
|
||||
expect(result).toContain("inline memory content");
|
||||
expect(result).toContain("### Long-term Workspace Memory");
|
||||
expect(result).toContain("workspace memory content");
|
||||
});
|
||||
|
||||
it("reads workspace memory using sanitized agent id", async () => {
|
||||
const weirdId = "Agent X/1";
|
||||
await mkdir(join(testDir, ".fusion", "agent-memory", "Agent-X-1"), { recursive: true });
|
||||
await writeFile(
|
||||
join(testDir, ".fusion", "agent-memory", "Agent-X-1", "MEMORY.md"),
|
||||
"sanitized workspace memory",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = await resolveAgentInstructions(makeAgent({ id: weirdId, memory: "" }), testDir);
|
||||
expect(result).toContain("sanitized workspace memory");
|
||||
expect(result).toContain("_Source: .fusion/agent-memory/Agent-X-1/MEMORY.md_");
|
||||
});
|
||||
|
||||
it("clamps oversized workspace memory", async () => {
|
||||
await mkdir(join(testDir, ".fusion", "agent-memory", "agent-test"), { recursive: true });
|
||||
await writeFile(
|
||||
join(testDir, ".fusion", "agent-memory", "agent-test", "MEMORY.md"),
|
||||
"x".repeat(60000),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = await resolveAgentInstructions(makeAgent({ memory: "" }), testDir);
|
||||
expect(result).toContain("x".repeat(50000));
|
||||
});
|
||||
|
||||
it("returns instructionsText when set", async () => {
|
||||
const agent = makeAgent({ instructionsText: "Always write tests." });
|
||||
const result = await resolveAgentInstructions(agent, testDir);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
createReadMessagesTool,
|
||||
createResearchTools,
|
||||
qmdAgentMemoryCollectionName,
|
||||
readAgentMemoryWorkspaceLongTerm,
|
||||
sendMessageParams,
|
||||
readMessagesParams,
|
||||
} from "../agent-tools.js";
|
||||
@@ -418,6 +419,45 @@ describe("createMemoryTools", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("readAgentMemoryWorkspaceLongTerm returns empty string when MEMORY.md is missing", async () => {
|
||||
await expect(readAgentMemoryWorkspaceLongTerm(tempDir, "ceo-agent")).resolves.toBe("");
|
||||
});
|
||||
|
||||
it("readAgentMemoryWorkspaceLongTerm returns trimmed MEMORY.md contents", async () => {
|
||||
const tools = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
|
||||
agentMemory: {
|
||||
agentId: "ceo-agent",
|
||||
agentName: "CEO",
|
||||
memory: "",
|
||||
},
|
||||
});
|
||||
const appendTool = tools.find((tool) => tool.name === "fn_memory_append")!;
|
||||
await (appendTool as any).execute("call-1", {
|
||||
scope: "agent",
|
||||
layer: "long-term",
|
||||
content: " durable memory content ",
|
||||
}, undefined, undefined, undefined);
|
||||
|
||||
await expect(readAgentMemoryWorkspaceLongTerm(tempDir, "ceo-agent")).resolves.toContain("durable memory content");
|
||||
});
|
||||
|
||||
it("readAgentMemoryWorkspaceLongTerm reads sanitized agent ids", async () => {
|
||||
const tools = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
|
||||
agentMemory: {
|
||||
agentId: "Agent X/1",
|
||||
agentName: "CEO",
|
||||
memory: "",
|
||||
},
|
||||
});
|
||||
const appendTool = tools.find((tool) => tool.name === "fn_memory_append")!;
|
||||
await (appendTool as any).execute("call-1", {
|
||||
scope: "agent",
|
||||
layer: "long-term",
|
||||
content: "- sanitized id memory",
|
||||
}, undefined, undefined, undefined);
|
||||
|
||||
await expect(readAgentMemoryWorkspaceLongTerm(tempDir, "Agent X/1")).resolves.toContain("sanitized id memory");
|
||||
});
|
||||
|
||||
it("logs a warning and continues when agent memory directory read fails", async () => {
|
||||
readdirMock.mockRejectedValueOnce(new Error("EACCES"));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import {
|
||||
@@ -1881,6 +1881,41 @@ describe("executeHeartbeat", () => {
|
||||
expect(callArgs.customTools![13]!.name).toBe("fn_heartbeat_done");
|
||||
});
|
||||
|
||||
it("loads workspace memory into system prompt and identity snapshot when inline memory is empty", async () => {
|
||||
const rootDir = mkdtempSync(join(tmpdir(), "heartbeat-workspace-memory-"));
|
||||
mkdirSync(join(rootDir, ".fusion", "agent-memory", "agent-001"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(rootDir, ".fusion", "agent-memory", "agent-001", "MEMORY.md"),
|
||||
"workspace memory for heartbeat",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
try {
|
||||
const store = createStoreWithAgentForExec({
|
||||
memory: "",
|
||||
instructionsText: undefined,
|
||||
instructionsPath: undefined,
|
||||
});
|
||||
const mockSession = createMockAgentSession();
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: mockSession as any,
|
||||
});
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir });
|
||||
|
||||
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
|
||||
expect(callArgs.systemPrompt).toContain("## Agent Memory");
|
||||
expect(callArgs.systemPrompt).toContain("workspace memory for heartbeat");
|
||||
|
||||
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0] as string;
|
||||
expect(executionPrompt).toMatch(/- memory: loaded \(\d+ chars, sha256:[a-f0-9]{8}, source: workspace\)/);
|
||||
} finally {
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("includes memory instructions even when agent has no custom instructions", async () => {
|
||||
const store = createStoreWithAgentForExec({
|
||||
soul: undefined,
|
||||
|
||||
@@ -637,7 +637,7 @@ describe("HeartbeatMonitor observability — prompt persistence + run-scoped log
|
||||
expect(exec).toContain("## Identity Snapshot");
|
||||
expect(exec).toContain("- agentId: agent-001");
|
||||
expect(exec).toMatch(/- soul: loaded \(\d+ chars, sha256:[0-9a-f]{8}\)/);
|
||||
expect(exec).toMatch(/- memory: loaded \(\d+ chars, sha256:[0-9a-f]{8}\)/);
|
||||
expect(exec).toMatch(/- memory: loaded \(\d+ chars, sha256:[0-9a-f]{8}(, source: (inline|workspace))?\)/);
|
||||
// Snapshot must NOT contain full preview content (that lives in the system prompt)
|
||||
expect(exec).not.toContain("I keep momentum across stalled tasks.");
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ApprovalRequestStore, buildExecutionMemoryInstructions, isEphemeralAgen
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createListAgentsTool, createDelegateTaskTool, createGetAgentConfigTool, createUpdateAgentConfigTool, createSendMessageTool, createReadMessagesTool, createMemoryTools, createReadEvaluationsTool, createUpdateIdentityTool, createReflectOnPerformanceTool, taskCreateParams } from "./agent-tools.js";
|
||||
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createListAgentsTool, createDelegateTaskTool, createGetAgentConfigTool, createUpdateAgentConfigTool, createSendMessageTool, createReadMessagesTool, createMemoryTools, createReadEvaluationsTool, createUpdateIdentityTool, createReflectOnPerformanceTool, readAgentMemoryWorkspaceLongTerm, taskCreateParams } from "./agent-tools.js";
|
||||
import { AgentLogger } from "./agent-logger.js";
|
||||
import {
|
||||
resolveAgentInstructionsWithRatings,
|
||||
@@ -490,16 +490,21 @@ function shortContentHash(value: string): string {
|
||||
function buildIdentitySnapshot(args: {
|
||||
agent: Agent;
|
||||
resolvedInstructions: string;
|
||||
workspaceMemory: string;
|
||||
}): string {
|
||||
const { agent, resolvedInstructions } = args;
|
||||
const { agent, resolvedInstructions, workspaceMemory } = args;
|
||||
|
||||
const soulTrimmed = typeof agent.soul === "string" ? agent.soul.trim() : "";
|
||||
const instrTrimmed = resolvedInstructions.trim();
|
||||
const memTrimmed = typeof agent.memory === "string" ? agent.memory.trim() : "";
|
||||
const inlineMemoryTrimmed = typeof agent.memory === "string" ? agent.memory.trim() : "";
|
||||
const workspaceMemoryTrimmed = workspaceMemory.trim();
|
||||
const memorySource = inlineMemoryTrimmed ? "inline" : workspaceMemoryTrimmed ? "workspace" : null;
|
||||
const memTrimmed = inlineMemoryTrimmed || workspaceMemoryTrimmed;
|
||||
|
||||
const formatField = (trimmed: string): string => {
|
||||
const formatField = (trimmed: string, source?: "inline" | "workspace"): string => {
|
||||
if (!trimmed) return "absent";
|
||||
return `loaded (${trimmed.length} chars, sha256:${shortContentHash(trimmed)})`;
|
||||
const sourceLabel = source ? `, source: ${source}` : "";
|
||||
return `loaded (${trimmed.length} chars, sha256:${shortContentHash(trimmed)}${sourceLabel})`;
|
||||
};
|
||||
|
||||
return [
|
||||
@@ -512,7 +517,7 @@ function buildIdentitySnapshot(args: {
|
||||
`- role: ${agent.role}`,
|
||||
`- soul: ${formatField(soulTrimmed)}`,
|
||||
`- instructions: ${formatField(instrTrimmed)}`,
|
||||
`- memory: ${formatField(memTrimmed)}`,
|
||||
`- memory: ${formatField(memTrimmed, memorySource ?? undefined)}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -1721,6 +1726,7 @@ export class HeartbeatMonitor {
|
||||
? HEARTBEAT_NO_TASK_SYSTEM_PROMPT
|
||||
: HEARTBEAT_SYSTEM_PROMPT;
|
||||
let resolvedInstructionsForIdentity = "";
|
||||
let workspaceMemoryForIdentity = "";
|
||||
try {
|
||||
resolvedInstructionsForIdentity = await resolveAgentInstructionsWithRatings(agent, rootDir, this.store);
|
||||
} catch (instructionError) {
|
||||
@@ -1728,6 +1734,13 @@ export class HeartbeatMonitor {
|
||||
heartbeatLog.warn(`Failed to resolve agent instructions for heartbeat ${agentId}: ${message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
workspaceMemoryForIdentity = await readAgentMemoryWorkspaceLongTerm(rootDir, agent.id);
|
||||
} catch (memoryReadErr) {
|
||||
const message = memoryReadErr instanceof Error ? memoryReadErr.message : String(memoryReadErr);
|
||||
heartbeatLog.warn(`Failed to resolve workspace memory for heartbeat ${agentId}: ${message}`);
|
||||
}
|
||||
|
||||
let memoryInstructions = "";
|
||||
if (memorySettings?.memoryEnabled !== false) {
|
||||
try {
|
||||
@@ -1895,7 +1908,11 @@ export class HeartbeatMonitor {
|
||||
`Heartbeat execution for agent "${agent.name}" (ID: ${agent.id})`,
|
||||
`Source: ${source}${triggerDetail ? ` (${triggerDetail})` : ""}`,
|
||||
"",
|
||||
buildIdentitySnapshot({ agent, resolvedInstructions: resolvedInstructionsForIdentity }),
|
||||
buildIdentitySnapshot({
|
||||
agent,
|
||||
resolvedInstructions: resolvedInstructionsForIdentity,
|
||||
workspaceMemory: workspaceMemoryForIdentity,
|
||||
}),
|
||||
"",
|
||||
"## Wake Delta",
|
||||
`- source: ${source}${triggerDetail ? ` (${triggerDetail})` : ""}`,
|
||||
@@ -2003,7 +2020,11 @@ export class HeartbeatMonitor {
|
||||
`Source: ${source}${triggerDetail ? ` (${triggerDetail})` : ""}`,
|
||||
`Assigned task: ${taskId} — ${taskTitle}`,
|
||||
"",
|
||||
buildIdentitySnapshot({ agent, resolvedInstructions: resolvedInstructionsForIdentity }),
|
||||
buildIdentitySnapshot({
|
||||
agent,
|
||||
resolvedInstructions: resolvedInstructionsForIdentity,
|
||||
workspaceMemory: workspaceMemoryForIdentity,
|
||||
}),
|
||||
"",
|
||||
"## Wake Delta",
|
||||
`- source: ${source}${triggerDetail ? ` (${triggerDetail})` : ""}`,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@fusion/core";
|
||||
import type { PluginRunner } from "./plugin-runner.js";
|
||||
import { createLogger } from "./logger.js";
|
||||
import { readAgentMemoryWorkspaceLongTerm } from "./agent-tools.js";
|
||||
|
||||
const log = createLogger("agent-instructions");
|
||||
|
||||
@@ -182,18 +183,39 @@ function formatSoulSection(soul: string, agentId: string): string {
|
||||
return `## Soul\n\n${trimmed}`;
|
||||
}
|
||||
|
||||
function formatMemorySection(memory: string, agentId: string): string {
|
||||
const trimmed = trimAndClamp(memory, MAX_MEMORY_LENGTH, "memory", agentId);
|
||||
if (!trimmed) {
|
||||
function memoryWorkspaceDisplayPath(agentId: string): string {
|
||||
const safeAgentId = agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
||||
return `.fusion/agent-memory/${safeAgentId}/MEMORY.md`;
|
||||
}
|
||||
|
||||
function formatMemorySection(memory: string, workspaceMemory: string, agentId: string): string {
|
||||
const inlineTrimmed = trimAndClamp(memory, MAX_MEMORY_LENGTH, "memory", agentId);
|
||||
const workspaceTrimmed = trimAndClamp(workspaceMemory, MAX_MEMORY_LENGTH, "workspace memory", agentId);
|
||||
if (!inlineTrimmed && !workspaceTrimmed) {
|
||||
return "";
|
||||
}
|
||||
return [
|
||||
|
||||
const lines = [
|
||||
"## Agent Memory",
|
||||
"",
|
||||
"This is memory for this agent only. Keep it separate from workspace Project Memory; use it for durable preferences, operating habits, and context that should follow this agent across tasks.",
|
||||
"Additional daily/dream files under .fusion/agent-memory/{agentId}/ are searchable with fn_memory_search.",
|
||||
"",
|
||||
trimmed,
|
||||
].join("\n");
|
||||
];
|
||||
|
||||
if (inlineTrimmed) {
|
||||
lines.push(inlineTrimmed);
|
||||
}
|
||||
|
||||
if (workspaceTrimmed) {
|
||||
if (!inlineTrimmed) {
|
||||
lines.push(`_Source: ${memoryWorkspaceDisplayPath(agentId)}_`, "", workspaceTrimmed);
|
||||
} else {
|
||||
lines.push("", "### Long-term Workspace Memory", "", workspaceTrimmed);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatPerformanceFeedbackSection(ratingSummary: AgentRatingSummary): string {
|
||||
@@ -293,11 +315,10 @@ export async function resolveAgentInstructions(
|
||||
}
|
||||
}
|
||||
|
||||
if (agent.memory?.trim()) {
|
||||
const memorySection = formatMemorySection(agent.memory, agent.id);
|
||||
if (memorySection) {
|
||||
parts.push(memorySection);
|
||||
}
|
||||
const workspaceMemory = await readAgentMemoryWorkspaceLongTerm(rootDir, agent.id);
|
||||
const memorySection = formatMemorySection(agent.memory ?? "", workspaceMemory, agent.id);
|
||||
if (memorySection) {
|
||||
parts.push(memorySection);
|
||||
}
|
||||
|
||||
if (ratingSummary && ratingSummary.totalRatings > 0) {
|
||||
|
||||
@@ -19,7 +19,6 @@ import { ResearchStepRunner } from "./research-step-runner.js";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import type { AgentReflectionService } from "./agent-reflection.js";
|
||||
import { MAX_INSTRUCTIONS_TEXT_LENGTH, MAX_MEMORY_LENGTH, MAX_SOUL_LENGTH } from "./agent-instructions.js";
|
||||
import { createLogger } from "./logger.js";
|
||||
|
||||
// ── Tool parameter schemas (canonical definitions) ────────────────────────
|
||||
@@ -194,6 +193,10 @@ type MemorySearchHit = {
|
||||
|
||||
const log = createLogger("agent-tools");
|
||||
|
||||
const MAX_INSTRUCTIONS_TEXT_LENGTH = 50_000;
|
||||
const MAX_MEMORY_LENGTH = 50_000;
|
||||
const MAX_SOUL_LENGTH = 10_000;
|
||||
|
||||
const AGENT_MEMORY_ROOT = ".fusion/agent-memory";
|
||||
const AGENT_MEMORY_FILENAME = "MEMORY.md";
|
||||
const AGENT_DREAMS_FILENAME = "DREAMS.md";
|
||||
@@ -201,11 +204,11 @@ const agentQmdRefreshState = new Map<string, { lastStartedAt: number; inFlight?:
|
||||
const AGENT_QMD_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
||||
const DAILY_AGENT_MEMORY_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
|
||||
|
||||
function sanitizeAgentMemoryId(agentId: string): string {
|
||||
export function sanitizeAgentMemoryId(agentId: string): string {
|
||||
return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
||||
}
|
||||
|
||||
function agentMemoryDisplayPath(agentId: string): string {
|
||||
export function agentMemoryDisplayPath(agentId: string): string {
|
||||
return `${AGENT_MEMORY_ROOT}/${sanitizeAgentMemoryId(agentId)}/${AGENT_MEMORY_FILENAME}`;
|
||||
}
|
||||
|
||||
@@ -217,7 +220,7 @@ function agentMemoryDirectory(rootDir: string, agentId: string): string {
|
||||
return join(rootDir, AGENT_MEMORY_ROOT, sanitizeAgentMemoryId(agentId));
|
||||
}
|
||||
|
||||
function agentMemoryFilePath(rootDir: string, agentId: string): string {
|
||||
export function agentMemoryFilePath(rootDir: string, agentId: string): string {
|
||||
return join(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME);
|
||||
}
|
||||
|
||||
@@ -229,6 +232,26 @@ function agentDailyFilePath(rootDir: string, agentId: string, date = new Date())
|
||||
return join(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
|
||||
}
|
||||
|
||||
export async function readAgentMemoryWorkspaceLongTerm(rootDir: string, agentId: string): Promise<string> {
|
||||
const safeRoot = typeof rootDir === "string" ? rootDir.trim() : "";
|
||||
const safeAgentId = typeof agentId === "string" ? agentId.trim() : "";
|
||||
if (!safeRoot || !safeAgentId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const filePath = agentMemoryFilePath(safeRoot, safeAgentId);
|
||||
try {
|
||||
const fileStat = await stat(filePath);
|
||||
if (!fileStat.isFile()) {
|
||||
return "";
|
||||
}
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
return typeof content === "string" ? content.trim() : "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function qmdAgentMemoryCollectionName(rootDir: string, agentId: string): string {
|
||||
const hash = createHash("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
|
||||
return `fusion-agent-memory-${sanitizeAgentMemoryId(agentId).toLowerCase()}-${hash}`;
|
||||
|
||||
Reference in New Issue
Block a user