How agents remember things and plan multi-step tasks
The Two Memory Systems in LangGraph
LangGraph has two completely separate memory systems. Most developers only know about one of them.
CHECKPOINTER (short-term memory)
→ Stores conversation history per thread_id
→ Like the chat history in ChatGPT
→ Lost when you clear the conversation
→ Scope: one conversation thread
STORE (long-term memory)
→ Stores facts across ALL conversations
→ User preferences, important facts, past decisions
→ Persists even when conversations change
→ Scope: cross-thread, user-level
Real world comparison:
Checkpointer = what you said in today's meeting
Store = what you know about this person from all past meetings
Project Setup
mkdir agent-memory-demo cd agent-memory-demo npm init -y
Update package.json:
{ "name": "agent-memory-demo", "version": "1.0.0", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { "@langchain/langgraph": "^1.4.8", "@langchain/openai": "^1.5.5", "dotenv": "^17.4.2", "langchain": "^1.5.4", "zod": "^4.4.3" } }
npm install langchain @langchain/openai @langchain/langgraph zod dotenv
Create .env:
OPENAI_API_KEY=sk-proj-your-key-here
Part 1 — Checkpointer (Short-Term Memory)
Create src/01_checkpointer.js:
import { createAgent, tool } from "langchain"; import { MemorySaver } from "@langchain/langgraph"; // MemorySaver = in-memory checkpointer // stores conversation snapshots in a JavaScript Map // data is lost when Node.js process stops // fine for learning — use SqliteSaver for production
import { ChatOpenAI } from "@langchain/openai"; import { z } from "zod"; import * as dotenv from "dotenv"; dotenv.config();
// ───────────────────────────────────────── // SETUP // ─────────────────────────────────────────
const checkpointer = new MemorySaver(); // checkpointer = stores conversation state snapshots // internally it is a Map: thread_id → serialized state // example internal state after a few messages: // { // "thread_abc": { // v: 1, // channel_values: { // messages: [ // HumanMessage { content: "My name is Sofia" }, // AIMessage { content: "Hello Sofia!" }, // HumanMessage { content: "I am a MERN developer" }, // AIMessage { content: "Great! MERN is a popular stack." } // ] // } // } // }
const simpleInfoTool = tool( async ({ topic }) => { const info = { "MERN": "MERN = MongoDB, Express, React, Node.js. A full-stack JavaScript framework.", "Next.js": "Next.js is a React framework for building full-stack web applications.", "AI": "AI = Artificial Intelligence. Machines that simulate human intelligence.", }; return info[topic] || `No info found for: ${topic}`; }, { name: "get_info", description: "Gets information about a tech topic.", schema: z.object({ topic: z.string() }), } );
const agent = createAgent({ model: new ChatOpenAI({ model: "gpt-4o", temperature: 0.1 }), tools: [simpleInfoTool], checkpointer, // passing checkpointer enables memory // without this → every .invoke() call is independent // with this → messages accumulate per thread_id systemPrompt: "You are a helpful assistant. Remember what the user tells you about themselves.", });
// ───────────────────────────────────────── // HELPER — send one message and get response // ─────────────────────────────────────────
async function chat(threadId, message) { // threadId = which conversation to continue // message = what the user is saying now
const result = await agent.invoke( { messages: [{ role: "user", content: message }] }, { configurable: { thread_id: threadId } } // thread_id = tells checkpointer which state to load/save // same thread_id = same conversation continues // different thread_id = fresh new conversation );
const lastMsg = result.messages[result.messages.length - 1]; // last message = AI's final response in this turn return lastMsg.content; }
// ───────────────────────────────────────── // EXAMPLE 1 — Memory within a conversation // ─────────────────────────────────────────
async function example1() { console.log("─".repeat(55)); console.log("EXAMPLE 1: Memory within one conversation"); console.log("─".repeat(55));
const threadId = "conversation_001"; // unique ID for this conversation // you choose the ID — could be user ID, session ID, UUID
// Turn 1 — introduce ourselves const r1 = await chat(threadId, "Hi! My name is Sofia and I am a MERN developer."); console.log("Turn 1:"); console.log("User: Hi! My name is Sofia and I am a MERN developer."); console.log("AI: ", r1);
// Turn 2 — follow up question const r2 = await chat(threadId, "What frameworks do you think I should learn next?"); console.log("\nTurn 2:"); console.log("User: What frameworks do you think I should learn next?"); console.log("AI: ", r2); // AI should mention MERN background because it remembers Turn 1
// Turn 3 — test memory const r3 = await chat(threadId, "What is my name and what do I do?"); console.log("\nTurn 3:"); console.log("User: What is my name and what do I do?"); console.log("AI: ", r3); // AI should say "Your name is Sofia and you are a MERN developer" // It remembers from Turn 1 — not from context window tricks
console.log(); }
// ───────────────────────────────────────── // EXAMPLE 2 — Different threads = different memories // ─────────────────────────────────────────
async function example2() { console.log("─".repeat(55)); console.log("EXAMPLE 2: Two separate users, separate memories"); console.log("─".repeat(55));
// User A's conversation await chat("user_alice", "My name is Alice and I love Python."); await chat("user_alice", "I work at Google.");
// User B's conversation — completely separate await chat("user_bob", "My name is Bob and I am a data scientist."); await chat("user_bob", "I live in Mumbai.");
// Now ask both about themselves const aliceAnswer = await chat("user_alice", "What do you know about me?"); const bobAnswer = await chat("user_bob", "What do you know about me?");
console.log("Alice's thread response:", aliceAnswer); console.log("Bob's thread response: ", bobAnswer); // Alice's thread: knows about Python, Google // Bob's thread: knows about data science, Mumbai // No mixing between threads ✅
console.log(); }
// ───────────────────────────────────────── // EXAMPLE 3 — Inspect what is stored in checkpointer // ─────────────────────────────────────────
async function example3() { console.log("─".repeat(55)); console.log("EXAMPLE 3: Inspecting checkpointer state"); console.log("─".repeat(55));
const threadId = "inspect_demo";
await chat(threadId, "My favorite color is blue."); await chat(threadId, "I enjoy hiking on weekends.");
// Get the stored state for this thread const state = await agent.getState( { configurable: { thread_id: threadId } } // same config format as invoke() ); // state.values = the current state of the agent for this thread // state.values.messages = array of all messages stored
const messages = state.values.messages; // example messages array: // [ // HumanMessage { content: "My favorite color is blue." }, // AIMessage { content: "That's a nice color!" }, // HumanMessage { content: "I enjoy hiking on weekends." }, // AIMessage { content: "Hiking is great exercise!" } // ]
console.log(`\nStored messages for thread "${threadId}":`); messages.forEach((msg, i) => { const role = msg.getType(); // getType() = "human", "ai", or "tool" const preview = msg.content.substring(0, 60); console.log(` ${i + 1}. [${role.padEnd(5)}] ${preview}`); });
console.log(`\nTotal messages stored: ${messages.length}`); console.log(); }
async function main() { console.log("🧠 CHECKPOINTER MEMORY DEMO\n"); await example1(); await example2(); await example3(); console.log("✅ Checkpointer demo complete!"); }
main().catch(console.error);
Run:
node src/01_checkpointer.js
Part 2 — Store (Long-Term Memory)
The Store persists facts across different conversations — this is what MemorySaver cannot do.
Create src/02_store.js:
import { createAgent } from "langchain"; import { MemorySaver, InMemoryStore } from "@langchain/langgraph"; // MemorySaver = short-term (conversation history per thread) // InMemoryStore = long-term (facts that survive across threads) // // In production replace InMemoryStore with: // → SqliteStore for local persistent storage // → PostgresStore for production database
import { ChatOpenAI } from "@langchain/openai"; import { tool } from "langchain"; import { z } from "zod"; import * as dotenv from "dotenv"; dotenv.config();
// ───────────────────────────────────────── // SETUP BOTH MEMORY SYSTEMS // ─────────────────────────────────────────
const checkpointer = new MemorySaver(); // short-term: conversation history per thread_id
const store = new InMemoryStore(); // long-term: facts that persist across ALL threads // InMemoryStore works like a key-value store // store.put(namespace, key, value) → save // store.get(namespace, key) → retrieve // store.search(namespace, query) → search
// ───────────────────────────────────────── // TOOLS THAT USE THE STORE // These tools let the agent read/write long-term memory // ─────────────────────────────────────────
const saveUserFactTool = tool( async ({ userId, fact, category }) => { // This tool saves an important fact about a user // to the long-term store — persists across conversations
const namespace = ["user_facts", userId]; // namespace = array of strings that acts like a folder path // example: ["user_facts", "sofia_123"] // all facts for this user go in this namespace
const key = `${category}_${Date.now()}`; // unique key for this fact // example: "preference_1752672000000"
await store.put(namespace, key, { fact, // the actual fact string // example: "prefers dark mode"
category, // what type of fact this is // example: "preference", "background", "goal"
savedAt: new Date().toISOString(), // when this was saved });
return `Saved fact for user ${userId}: "${fact}" (category: ${category})`; // confirmation string back to the agent }, { name: "save_user_fact", description: `Saves an important fact about the user to long-term memory. Use this when the user shares something important about themselves: preferences, background, goals, or any fact worth remembering across conversations.`, schema: z.object({ userId: z.string().describe("the user's ID"), fact: z.string().describe("the fact to remember"), category: z.enum(["preference", "background", "goal", "general"]) .describe("what type of fact this is"), }), } );
const getUserFactsTool = tool( async ({ userId }) => { // Retrieves all saved facts for a user from long-term store
const namespace = ["user_facts", userId]; // same namespace as saveUserFactTool // example: ["user_facts", "sofia_123"]
const results = await store.search(namespace, { query: "" }); // store.search() = search within this namespace // query: "" = return all items (empty query = no filter) // // example results: // [ // { key: "preference_123", value: { fact: "prefers dark mode", category: "preference", savedAt: "..." } }, // { key: "background_456", value: { fact: "MERN developer", category: "background", savedAt: "..." } }, // ]
if (!results || results.length === 0) { return `No stored facts found for user ${userId}`; }
const facts = results.map(item => `[${item.value.category}] ${item.value.fact}` ).join("\n"); // format each fact as "[category] fact text" // example: "[preference] prefers dark mode\n[background] MERN developer"
return `Known facts about user ${userId}:\n${facts}`; }, { name: "get_user_facts", description: `Retrieves all known facts about a user from long-term memory. Use this at the start of a conversation to personalize responses based on what you know about this user from previous conversations.`, schema: z.object({ userId: z.string().describe("the user's ID to look up"), }), } );
// ───────────────────────────────────────── // CREATE AGENT WITH BOTH MEMORY SYSTEMS // ─────────────────────────────────────────
const agent = createAgent({ model: new ChatOpenAI({ model: "gpt-4o", temperature: 0.1 }), tools: [saveUserFactTool, getUserFactsTool], checkpointer, // short-term: remembers this conversation store, // long-term: reads/writes cross-conversation facts systemPrompt: `You are a personalized assistant that remembers users.
At the start of each conversation: 1. Call get_user_facts to load what you know about this user 2. Use that context to personalize your responses
During conversation: 3. When user shares something important → call save_user_fact 4. Categories: preference (what they like), background (who they are), goal (what they want to achieve), general (other facts)
Be natural about using memory — don't announce every save.`, });
// ───────────────────────────────────────── // HELPER // ─────────────────────────────────────────
async function chat(threadId, message) { const result = await agent.invoke( { messages: [{ role: "user", content: message }] }, { configurable: { thread_id: threadId } } ); return result.messages[result.messages.length - 1].content; }
// ───────────────────────────────────────── // DEMO — Shows memory persisting across separate conversations // ─────────────────────────────────────────
async function demo() { console.log("─".repeat(55)); console.log("DEMO: Long-term memory across conversations"); console.log("─".repeat(55));
const userId = "sofia_123";
// ── CONVERSATION 1 ────────────────────────────────────── console.log("\n📅 CONVERSATION 1 (thread: conv_001)"); console.log("─".repeat(40));
const c1r1 = await chat("conv_001", `Hello! I am ${userId}. I am a MERN stack developer learning AI engineering.` ); console.log("User: Hello! I am sofia_123. I am a MERN developer learning AI."); console.log("AI: ", c1r1);
const c1r2 = await chat("conv_001", "I prefer learning through hands-on projects rather than theory." ); console.log("\nUser: I prefer learning through hands-on projects."); console.log("AI: ", c1r2); // Agent should save these facts to long-term store
// ── CONVERSATION 2 (new thread — different session) ───── console.log("\n📅 CONVERSATION 2 (thread: conv_002 — new session)"); console.log("─".repeat(40)); console.log("(This is a completely new conversation — checkpointer has no history here)");
const c2r1 = await chat("conv_002", `Hi, I am ${userId} again. What do you know about me?` ); console.log(`\nUser: Hi, I am ${userId} again. What do you know about me?`); console.log("AI: ", c2r1); // Agent loads facts from store even though this is a new thread // Should mention MERN background and learning preference
const c2r2 = await chat("conv_002", "Can you suggest what I should focus on next in my AI learning journey?" ); console.log("\nUser: Can you suggest what to focus on next in AI learning?"); console.log("AI: ", c2r2); // Agent uses stored facts to give personalized recommendation
console.log(); }
async function main() { console.log("💾 LONG-TERM STORE MEMORY DEMO\n"); await demo(); console.log("✅ Store memory demo complete!"); }
main().catch(console.error);
Run:
node src/02_store.js
Part 3 — Agent Planning
Planning = agent breaks a complex task into steps before acting.
Create src/03_planning.js:
import { createAgent, tool } from "langchain"; import { MemorySaver } from "@langchain/langgraph"; import { ChatOpenAI } from "@langchain/openai"; import { z } from "zod"; import * as dotenv from "dotenv"; dotenv.config();
// ───────────────────────────────────────── // TOOLS FOR A RESEARCH PLANNING AGENT // ─────────────────────────────────────────
const searchTool = tool( async ({ query }) => { // Simulated web search console.log(` 🔍 Searching: "${query}"`); await new Promise(r => setTimeout(r, 200)); // simulate network delay
const results = { "LangChain architecture 2025": "LangChain 1.x uses LangGraph as the agent backend. Key components: createAgent, tool(), MemorySaver, InMemoryStore.", "RAG best practices": "RAG best practices: chunk size 500-1000 chars, overlap 10-20%, MMR retrieval, reranking for quality, score thresholds.", "vector database comparison": "Pinecone: managed, easy setup. Chroma: open source, local. Qdrant: high performance. pgvector: PostgreSQL extension.", "AI agent patterns": "Common patterns: ReAct (reason+act), Plan-and-Execute, Reflexion, MRKL, self-ask with search.", };
return results[query] || `Search results for "${query}": [simulated research findings about this topic]`; }, { name: "search", description: "Searches the web for information on a topic.", schema: z.object({ query: z.string().describe("search query") }), } );
const summarizeTool = tool( async ({ text, maxSentences }) => { // Simulated summarizer console.log(` 📝 Summarizing text (${text.length} chars → ${maxSentences} sentences)`); return `Summary (${maxSentences} sentences): This is a condensed version of the provided text focusing on the key points.`; }, { name: "summarize", description: "Summarizes a piece of text into a specified number of sentences.", schema: z.object({ text: z.string().describe("text to summarize"), maxSentences: z.number().describe("maximum sentences in summary"), }), } );
const writeReportTool = tool( async ({ title, sections }) => { // Compiles collected information into a report console.log(` 📄 Writing report: "${title}" with ${sections.length} sections`);
const report = ` # ${title}
${sections.map((s, i) => `## ${i + 1}. ${s.heading}\n${s.content}`).join("\n\n")}
--- Report generated by AI Research Agent `.trim();
return report; }, { name: "write_report", description: "Compiles research findings into a formatted report.", schema: z.object({ title: z.string().describe("report title"), sections: z.array(z.object({ heading: z.string().describe("section heading"), content: z.string().describe("section content"), })).describe("report sections"), }), } );
// ───────────────────────────────────────── // PLANNING AGENT // System prompt teaches it to plan first then act // ─────────────────────────────────────────
const planningAgent = createAgent({ model: new ChatOpenAI({ model: "gpt-4o", temperature: 0.1 }), tools: [searchTool, summarizeTool, writeReportTool], checkpointer: new MemorySaver(), systemPrompt: `You are a research agent that plans before acting.
When given a research task: STEP 1 — PLAN: First explain your plan in 3-5 steps STEP 2 — SEARCH: Gather information using the search tool STEP 3 — PROCESS: Summarize and organize findings STEP 4 — REPORT: Write a final report using write_report
Always follow this order. Do not skip planning. Be systematic and thorough.`, });
// ───────────────────────────────────────── // RUN THE PLANNING AGENT // ─────────────────────────────────────────
async function runResearchTask(task) { console.log("\n" + "=".repeat(55)); console.log("Research Task:", task); console.log("=".repeat(55));
const result = await planningAgent.invoke( { messages: [{ role: "user", content: task }] }, { configurable: { thread_id: `research_${Date.now()}` } } );
console.log("\n📋 FINAL OUTPUT:"); console.log("─".repeat(55)); console.log(result.messages[result.messages.length - 1].content); }
async function main() { console.log("📊 AGENT PLANNING DEMO\n");
await runResearchTask( "Research the topic of RAG systems and create a short report covering: what RAG is, best practices, and which vector database to choose." );
console.log("\n✅ Planning demo complete!"); }
main().catch(console.error);
Run:
node src/03_planning.js
Memory Types — Summary Table
┌──────────────────┬────────────────────┬─────────────────────┐
│ Type │ LangGraph Object │ Use Case │
├──────────────────┼────────────────────┼─────────────────────┤
│ Short-term │ MemorySaver │ Conversation │
│ (in-memory) │ │ history in one chat │
├──────────────────┼────────────────────┼─────────────────────┤
│ Short-term │ SqliteSaver │ Conversation │
│ (persistent) │ │ history + restarts │
├──────────────────┼────────────────────┼─────────────────────┤
│ Long-term │ InMemoryStore │ User facts across │
│ (in-memory) │ │ all conversations │
├──────────────────┼────────────────────┼─────────────────────┤
│ Long-term │ SqliteStore / │ Production user │
│ (persistent) │ PostgresStore │ preferences, facts │
├──────────────────┼────────────────────┼─────────────────────┤
│ Semantic │ Vector Store │ Similarity-based │
│ (search-based) │ (Pinecone etc.) │ document retrieval │
└──────────────────┴────────────────────┴─────────────────────┘
3-Line Summary
- LangGraph has two separate memory systems — the Checkpointer stores conversation history per thread (short-term, like chat history) and the Store persists facts across all threads (long-term, like user preferences) — use both together for fully personalized agents.
MemorySaverandInMemoryStoreare for development only — data is lost when the process stops — for production useSqliteSaver(local file) orPostgresSaver(database) for conversations andSqliteStore/PostgresStorefor long-term facts.- Agent planning means giving the agent a system prompt that instructs it to outline its steps before acting — this dramatically improves quality on complex multi-step tasks because the agent reasons about the full approach before calling any tools.
Module 8.3 — Complete ✅
Coming up — Module 8.4 — Multi-Agent Systems with LangGraph
Building systems where multiple specialized agents collaborate — one agent researches, another writes, another reviews. This is how production AI systems handle complex tasks that are too big for one agent.
No comments:
Post a Comment