Haystack Tutorial (TypeScript): adding memory to agents for intermediate developers
This tutorial shows you how to give a Haystack agent persistent memory in TypeScript so it can remember prior turns, user preferences, and task context across multiple calls. You need this when a single prompt is not enough and your agent has to behave like a real assistant instead of a stateless chat function.
What You'll Need
- •Node.js 18+ and npm
- •A TypeScript project with
ts-nodeor a build step - •Haystack JS packages:
- •
@haystack-ai/core - •
@haystack-ai/agents
- •
- •An OpenAI API key set as
OPENAI_API_KEY - •A basic Haystack agent already working in TypeScript
- •A place to store memory state for the demo, such as an in-memory array or a file
Step-by-Step
- •Start by installing the packages and setting up a minimal TypeScript project. For this tutorial, we’ll use an in-memory memory store first, because it makes the agent behavior easy to inspect before you move to Redis or a database.
npm init -y
npm install @haystack-ai/core @haystack-ai/agents dotenv
npm install -D typescript ts-node @types/node
- •Create your environment file and TypeScript config. Keep the API key out of source control, and make sure your compiler target is modern enough for async/await.
cat > .env << 'EOF'
OPENAI_API_KEY=your_openai_api_key_here
EOF
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}
EOF
mkdir -p src
- •Build a small memory layer that stores conversation turns per session. In production you would persist this somewhere durable, but the shape stays the same: session ID in, recent messages out.
// src/memory.ts
export type MemoryTurn = {
role: "user" | "assistant";
content: string;
};
const sessions = new Map<string, MemoryTurn[]>();
export function appendToMemory(sessionId: string, turn: MemoryTurn): void {
const existing = sessions.get(sessionId) ?? [];
sessions.set(sessionId, [...existing, turn]);
}
export function getMemory(sessionId: string): MemoryTurn[] {
return sessions.get(sessionId) ?? [];
}
export function formatMemory(sessionId: string): string {
return getMemory(sessionId)
.map((turn) => `${turn.role.toUpperCase()}: ${turn.content}`)
.join("\n");
}
- •Create the agent wrapper that injects memory into each prompt and writes new turns back after every response. This is the core pattern: retrieve context, send it to the model, then persist the latest exchange.
// src/agent.ts
import "dotenv/config";
import { OpenAIChatGenerator } from "@haystack-ai/core";
import { appendToMemory, formatMemory } from "./memory.js";
const generator = new OpenAIChatGenerator({
apiKey: process.env.OPENAI_API_KEY!,
model: "gpt-4o-mini",
});
export async function chatWithMemory(sessionId: string, userMessage: string) {
const memory = formatMemory(sessionId);
const messages = [
{
role: "system" as const,
content:
"You are a helpful assistant. Use conversation history to stay consistent.",
},
...(memory ? [{ role: "system" as const, content: `Conversation history:\n${memory}` }] : []),
{ role: "user" as const, content: userMessage },
];
const response = await generator.run({ messages });
const answer = response.content;
appendToMemory(sessionId, { role: "user", content: userMessage });
appendToMemory(sessionId, { role: "assistant", content: answer });
return answer;
}
- •Add a simple CLI runner so you can test whether the agent actually remembers previous turns. The important part is using the same session ID across calls; that’s what makes memory visible.
// src/index.ts
import { chatWithMemory } from "./agent.js";
async function main() {
const sessionId = "customer-123";
const first = await chatWithMemory(
sessionId,
"My name is Sarah and I prefer short answers."
);
console.log("\nFIRST RESPONSE:\n", first);
const second = await chatWithMemory(
sessionId,
"What is my name and how should you answer me?"
);
console.log("\nSECOND RESPONSE:\n", second);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
- •Run it with TypeScript execution and confirm that the second response reflects earlier context. If it does not mention Sarah or short answers, your memory injection or session handling is wrong.
npx ts-node src/index.ts
Testing It
Run the script twice with the same sessionId and verify that later responses still know earlier facts like names or preferences. Then change the sessionId and confirm that memory does not leak between users.
A good test is to ask for something specific on turn one, then refer back to it indirectly on turn two:
- •“My project deadline is Friday.”
- •“When is my deadline?”
If the agent answers “Friday,” your memory pipeline is working.
For production-style validation, inspect both sides of the flow:
- •The prompt sent to the model includes prior turns.
- •The stored memory contains both user and assistant messages in order.
Next Steps
- •Replace the in-memory
Mapwith Redis or Postgres so memory survives restarts. - •Add summarization when history gets too long so prompts stay within token limits.
- •Store separate memories per customer profile, case ID, or support ticket instead of one flat chat history.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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