Skip to content

Commit 0fe09a5

Browse files
fix: missing files/memoryContents in memory middleware (#159)
* Update memory state schmea * Update tests * Update testing * changeset * Update libs/deepagents/src/middleware/memory.test.ts * Update libs/deepagents/src/middleware/memory.test.ts * Update libs/deepagents/src/middleware/memory.test.ts * Update libs/deepagents/src/middleware/memory.test.ts * Update libs/deepagents/src/middleware/memory.test.ts * Update libs/deepagents/src/middleware/memory.test.ts --------- Co-authored-by: Christian Bromann <git@bromann.dev>
1 parent 0ef8362 commit 0fe09a5

File tree

4 files changed

+252
-2
lines changed

4 files changed

+252
-2
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"deepagents": patch
3+
---
4+
5+
fix(deepagents): fix memoryMiddleware for statebacken
6+
- Export FileDataSchema for reuse.
7+
- Add files to MemoryStateSchema via StateSchema/ReducedValue.
8+
- Add StateBackend memory tests mirroring skills flow.
9+

libs/deepagents/src/middleware/fs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ import type * as _messages from "@langchain/core/messages";
131131
/**
132132
* Zod v3 schema for FileData (re-export from backends)
133133
*/
134-
const FileDataSchema = z.object({
134+
export const FileDataSchema = z.object({
135135
content: z.array(z.string()),
136136
created_at: z.string(),
137137
modified_at: z.string(),

libs/deepagents/src/middleware/memory.test.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import type {
44
BackendProtocol,
55
FileDownloadResponse,
66
} from "../backends/protocol.js";
7+
import { createDeepAgent } from "../agent.js";
8+
import { FakeListChatModel } from "@langchain/core/utils/testing";
9+
import {
10+
HumanMessage,
11+
SystemMessage,
12+
type BaseMessage,
13+
} from "@langchain/core/messages";
14+
import { MemorySaver } from "@langchain/langgraph";
15+
import { createFileData } from "../backends/utils.js";
716

817
// Mock backend that returns specified files
918
function createMockBackend(
@@ -261,3 +270,226 @@ describe("createMemoryMiddleware", () => {
261270
});
262271
});
263272
});
273+
274+
/**
275+
* StateBackend integration tests.
276+
*
277+
* These tests verify that memory is properly loaded from state.files and
278+
* injected into the system prompt when using createDeepAgent with StateBackend.
279+
*/
280+
describe("StateBackend integration with createDeepAgent", () => {
281+
const USER_MEMORY = `# User Memory
282+
283+
Remember: The secret code is ALPHA123.`;
284+
285+
const PROJECT_MEMORY = `# Project Memory
286+
287+
This project uses React.`;
288+
289+
/**
290+
* Helper to extract system prompt content from model invoke spy.
291+
* The system message can have content as string or array of content blocks.
292+
*/
293+
function getSystemPromptFromSpy(
294+
invokeSpy: ReturnType<typeof vi.spyOn>,
295+
): string {
296+
const lastCall = invokeSpy.mock.calls[invokeSpy.mock.calls.length - 1];
297+
const messages = lastCall?.[0] as BaseMessage[] | undefined;
298+
if (!messages) return "";
299+
const systemMessage = messages.find(SystemMessage.isInstance);
300+
if (!systemMessage) return "";
301+
return systemMessage.text;
302+
}
303+
304+
it("should load memory from state.files and inject into system prompt", async () => {
305+
const invokeSpy = vi.spyOn(FakeListChatModel.prototype, "invoke");
306+
const model = new FakeListChatModel({ responses: ["Done"] });
307+
const checkpointer = new MemorySaver();
308+
309+
const agent = createDeepAgent({
310+
model: model as any,
311+
memory: ["/AGENTS.md"],
312+
checkpointer,
313+
});
314+
315+
await agent.invoke(
316+
{
317+
messages: [new HumanMessage("What do you remember?")],
318+
files: {
319+
"/AGENTS.md": createFileData(USER_MEMORY),
320+
},
321+
},
322+
{
323+
configurable: { thread_id: `test-memory-${Date.now()}` },
324+
recursionLimit: 50,
325+
},
326+
);
327+
328+
expect(invokeSpy).toHaveBeenCalled();
329+
const systemPrompt = getSystemPromptFromSpy(invokeSpy);
330+
331+
expect(systemPrompt).toContain("ALPHA123");
332+
expect(systemPrompt).toContain("/AGENTS.md");
333+
invokeSpy.mockRestore();
334+
});
335+
336+
it("should load multiple memory files from state.files", async () => {
337+
const invokeSpy = vi.spyOn(FakeListChatModel.prototype, "invoke");
338+
const model = new FakeListChatModel({ responses: ["Done"] });
339+
const checkpointer = new MemorySaver();
340+
341+
const agent = createDeepAgent({
342+
model: model as any,
343+
memory: ["/user/AGENTS.md", "/project/AGENTS.md"],
344+
checkpointer,
345+
});
346+
347+
await agent.invoke(
348+
{
349+
messages: [new HumanMessage("List all memory")],
350+
files: {
351+
"/user/AGENTS.md": createFileData(USER_MEMORY),
352+
"/project/AGENTS.md": createFileData(PROJECT_MEMORY),
353+
},
354+
},
355+
{
356+
configurable: { thread_id: `test-memory-multi-${Date.now()}` },
357+
recursionLimit: 50,
358+
},
359+
);
360+
361+
expect(invokeSpy).toHaveBeenCalled();
362+
const systemPrompt = getSystemPromptFromSpy(invokeSpy);
363+
364+
expect(systemPrompt).toContain("ALPHA123");
365+
expect(systemPrompt).toContain("This project uses React.");
366+
invokeSpy.mockRestore();
367+
});
368+
369+
it("should show no memory message when state.files is empty", async () => {
370+
const invokeSpy = vi.spyOn(FakeListChatModel.prototype, "invoke");
371+
const model = new FakeListChatModel({ responses: ["Done"] });
372+
const checkpointer = new MemorySaver();
373+
374+
const agent = createDeepAgent({
375+
model: model as any,
376+
memory: ["/AGENTS.md"],
377+
checkpointer,
378+
});
379+
380+
await agent.invoke(
381+
{
382+
messages: [new HumanMessage("Hello")],
383+
files: {},
384+
},
385+
{
386+
configurable: { thread_id: `test-memory-empty-${Date.now()}` },
387+
recursionLimit: 50,
388+
},
389+
);
390+
391+
expect(invokeSpy).toHaveBeenCalled();
392+
const systemPrompt = getSystemPromptFromSpy(invokeSpy);
393+
394+
expect(systemPrompt).toContain("(No memory loaded)");
395+
invokeSpy.mockRestore();
396+
});
397+
398+
it("should load memory from multiple sources via StateBackend", async () => {
399+
const invokeSpy = vi.spyOn(FakeListChatModel.prototype, "invoke");
400+
const model = new FakeListChatModel({ responses: ["Done"] });
401+
const checkpointer = new MemorySaver();
402+
403+
const agent = createDeepAgent({
404+
model: model as any,
405+
memory: ["/memory/user/AGENTS.md", "/memory/project/AGENTS.md"],
406+
checkpointer,
407+
});
408+
409+
await agent.invoke(
410+
{
411+
messages: [new HumanMessage("List memory")],
412+
files: {
413+
"/memory/user/AGENTS.md": createFileData(USER_MEMORY),
414+
"/memory/project/AGENTS.md": createFileData(PROJECT_MEMORY),
415+
},
416+
},
417+
{
418+
configurable: { thread_id: `test-memory-sources-${Date.now()}` },
419+
recursionLimit: 50,
420+
},
421+
);
422+
423+
expect(invokeSpy).toHaveBeenCalled();
424+
const systemPrompt = getSystemPromptFromSpy(invokeSpy);
425+
426+
expect(systemPrompt).toContain("/memory/user/AGENTS.md");
427+
expect(systemPrompt).toContain("/memory/project/AGENTS.md");
428+
invokeSpy.mockRestore();
429+
});
430+
431+
it("should include memory paths in the system prompt", async () => {
432+
const invokeSpy = vi.spyOn(FakeListChatModel.prototype, "invoke");
433+
const model = new FakeListChatModel({ responses: ["Done"] });
434+
const checkpointer = new MemorySaver();
435+
436+
const agent = createDeepAgent({
437+
model: model as any,
438+
memory: ["/AGENTS.md"],
439+
checkpointer,
440+
});
441+
442+
await agent.invoke(
443+
{
444+
messages: [new HumanMessage("What memory do you have?")],
445+
files: {
446+
"/AGENTS.md": createFileData(USER_MEMORY),
447+
},
448+
},
449+
{
450+
configurable: { thread_id: `test-memory-paths-${Date.now()}` },
451+
recursionLimit: 50,
452+
},
453+
);
454+
455+
expect(invokeSpy).toHaveBeenCalled();
456+
const systemPrompt = getSystemPromptFromSpy(invokeSpy);
457+
458+
expect(systemPrompt).toContain("/AGENTS.md");
459+
expect(systemPrompt).toContain("<memory_guidelines>");
460+
invokeSpy.mockRestore();
461+
});
462+
463+
it("should handle empty memory directory gracefully", async () => {
464+
const invokeSpy = vi.spyOn(FakeListChatModel.prototype, "invoke");
465+
const model = new FakeListChatModel({ responses: ["Done"] });
466+
const checkpointer = new MemorySaver();
467+
468+
const agent = createDeepAgent({
469+
model: model as any,
470+
memory: ["/memory/empty/AGENTS.md"],
471+
checkpointer,
472+
});
473+
474+
await expect(
475+
agent.invoke(
476+
{
477+
messages: [new HumanMessage("Hello")],
478+
files: {},
479+
},
480+
{
481+
configurable: {
482+
thread_id: `test-memory-empty-graceful-${Date.now()}`,
483+
},
484+
recursionLimit: 50,
485+
},
486+
),
487+
).resolves.toBeDefined();
488+
489+
expect(invokeSpy).toHaveBeenCalled();
490+
const systemPrompt = getSystemPromptFromSpy(invokeSpy);
491+
492+
expect(systemPrompt).toContain("(No memory loaded)");
493+
invokeSpy.mockRestore();
494+
});
495+
});

libs/deepagents/src/middleware/memory.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ import {
6060
import type { BackendProtocol, BackendFactory } from "../backends/protocol.js";
6161
import type { StateBackend } from "../backends/state.js";
6262
import type { BaseStore } from "@langchain/langgraph-checkpoint";
63+
import { fileDataReducer, FileDataSchema } from "./fs.js";
64+
import { ReducedValue, StateSchema } from "@langchain/langgraph";
6365

6466
/**
6567
* Options for the memory middleware.
@@ -85,12 +87,19 @@ export interface MemoryMiddlewareOptions {
8587
/**
8688
* State schema for memory middleware.
8789
*/
88-
const MemoryStateSchema = z.object({
90+
const MemoryStateSchema = new StateSchema({
8991
/**
9092
* Dict mapping source paths to their loaded content.
9193
* Marked as private so it's not included in the final agent state.
9294
*/
9395
memoryContents: z.record(z.string(), z.string()).optional(),
96+
files: new ReducedValue(
97+
z.record(z.string(), FileDataSchema).default(() => ({})),
98+
{
99+
inputSchema: z.record(z.string(), FileDataSchema.nullable()).optional(),
100+
reducer: fileDataReducer,
101+
},
102+
),
94103
});
95104

96105
/**

0 commit comments

Comments
 (0)