@@ -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
918function 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+ } ) ;
0 commit comments