LangChain Tutorial (TypeScript): adding memory to agents for advanced developers

By Cyprian AaronsUpdated 2026-04-21
langchainadding-memory-to-agents-for-advanced-developerstypescript

This tutorial shows how to add durable conversational memory to a LangChain agent in TypeScript, so the agent can keep context across turns instead of treating every request like a fresh session. You need this when you’re building support bots, underwriting assistants, claims workflows, or any agent that must remember prior decisions, customer details, and intermediate state.

What You'll Need

  • Node.js 18+
  • TypeScript 5+
  • langchain
  • @langchain/openai
  • An OpenAI API key in OPENAI_API_KEY
  • A project configured for ESM or ts-node/tsx
  • Basic familiarity with LangChain agents and tools

Install the packages:

npm install langchain @langchain/openai
npm install -D typescript tsx @types/node

Set your environment variable:

export OPENAI_API_KEY="your-key-here"

Step-by-Step

  1. Start with a simple chat model and a message history store.

For production systems, memory should be explicit. Don’t hide it inside a global variable; make the session boundary obvious so you can swap in Redis, Postgres, or DynamoDB later.

import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, AIMessage } from "@langchain/core/messages";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

const history: Array<HumanMessage | AIMessage> = [];
  1. Build a helper that injects prior messages into each call.

This is the core pattern: load history, append the new user input, call the model, then persist both sides of the exchange. That gives you deterministic memory behavior without relying on deprecated abstractions.

async function chatWithMemory(userInput: string) {
  const messages = [...history, new HumanMessage(userInput)];
  const response = await model.invoke(messages);

  history.push(new HumanMessage(userInput));
  history.push(response);

  return response.content;
}
  1. Wrap the model in an agent-style loop with tool support.

If you’re using agents, memory needs to survive tool calls too. The simplest reliable approach is to keep a session-scoped message list and pass it through every agent turn.

import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";

const lookupPolicyTool = new DynamicStructuredTool({
  name: "lookup_policy",
  description: "Look up policy status by policy number.",
  schema: z.object({
    policyNumber: z.string(),
  }),
  func: async ({ policyNumber }) => {
    return `Policy ${policyNumber} is active and renews on 2026-01-15`;
  },
});
  1. Create an agent loop that preserves conversation state across turns.

This example uses plain messages so you can see exactly what gets stored. In real systems, this structure maps cleanly to a session record in Redis or a database table keyed by user ID.

async function runAgentTurn(userInput: string) {
  const messages = [
    ...history,
    new HumanMessage(
      `You are a helpful insurance assistant. Use tools when needed.\nUser: ${userInput}`
    ),
  ];

  const response = await model.bindTools([lookupPolicyTool]).invoke(messages);

  history.push(new HumanMessage(userInput));
  history.push(response);

  return response.content;
}
  1. Add retrieval-style memory trimming so prompts don’t grow forever.

Long-running agent sessions will hit token limits if you keep every message forever. A practical pattern is to retain only the last N messages or summarize older turns before sending them back to the model.

function trimHistory(maxMessages = 12) {
  if (history.length <= maxMessages) return;
  history.splice(0, history.length - maxMessages);
}

async function runTrimmedTurn(userInput: string) {
  trimHistory(12);
  const response = await chatWithMemory(userInput);
  trimHistory(12);
  return response;
}
  1. Put it together in a runnable script.

This gives you a minimal memory-enabled agent flow you can extend with real tools and persistent storage. Keep the interface stable now; swap the backing store later without changing your orchestration code.

async function main() {
  console.log(await runTrimmedTurn("My policy number is POL-12345."));
  console.log(await runTrimmedTurn("What is my policy status?"));
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Testing It

Run the script twice in the same process and verify that follow-up questions use prior context. For example, after telling it your policy number, ask “What is my policy status?” and confirm it refers to POL-12345 without repeating it manually.

If you want to test real persistence behavior, move history into a session map keyed by user ID and simulate two separate users. Each user should get isolated memory with no cross-contamination.

Also test prompt growth by sending a long back-and-forth conversation and confirming trimHistory() keeps your message count bounded. In production, replace trimming with summarization once conversations become operationally important.

Next Steps

  • Replace the in-memory array with Redis-backed message history for multi-instance deployments.
  • Add conversation summarization for long-running claims or support cases.
  • Move from raw message passing to LangChain’s higher-level agent executor patterns once your memory strategy is stable.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides