140 lines
7.7 KiB
Markdown
140 lines
7.7 KiB
Markdown
# Agent memory
|
|
|
|
Memory is a sub-node on the agent, wired via `ai_memory`. Without it, every invocation is stateless. With it, the agent holds a conversation across turns — and across executions, depending on type — keyed by whatever expression you bind to `sessionKey`.
|
|
|
|
Memory node availability shifts between n8n versions, so confirm what's installed with `search_nodes({ query: 'memory' })`.
|
|
|
|
---
|
|
|
|
## The two non-negotiables
|
|
|
|
1. **Plumb a stable key through.** Memory buckets by whatever you bind to `sessionKey`. The Chat Trigger fills `sessionId` automatically. For other triggers, derive a stable identifier (Slack `thread_ts`, a webhook conversation ID, a generated UUID, a multi-tenant composite) and forward it to memory and any session-keyed tools. Without consistency across the same conversation, memory never matches.
|
|
2. **Default to `memoryBufferWindow`.** It persists across executions via n8n's internal store, keyed on `sessionKey`, and is the right choice for nearly every chat agent. Reach for Postgres/Redis only when memory must be read **outside** the agent.
|
|
|
|
---
|
|
|
|
## The memory types
|
|
|
|
### `memoryBufferWindow` (the default)
|
|
|
|
In-context memory of the last N exchanges, persisted across executions via n8n's store.
|
|
|
|
```json
|
|
{
|
|
"parameters": {
|
|
"sessionIdType": "customKey",
|
|
"sessionKey": "={{ $json.sessionId }}",
|
|
"contextWindowLength": 50
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
|
"typeVersion": 1.3,
|
|
"name": "Simple Memory"
|
|
}
|
|
```
|
|
|
|
`contextWindowLength` is the number of exchanges retained. **The default is 5 — very low** for modern chat expectations, where users assume a conversation feels close to endless. **50 is a reasonable starting point.** Higher = more context but more tokens per turn.
|
|
|
|
**Messages past the window are removed entirely.** Once the buffer fills, the oldest exchanges are dropped and the agent can't recall, search, or even know they existed. If a user said something 60 turns ago and the window is 50, that's gone from the agent's perspective. For recall beyond the window, raise `contextWindowLength`, or persist key facts in a Data Table that's read and injected into the system prompt.
|
|
|
|
The "window" is a sliding cap on how many messages stay in context — **not** a scope on persistence. With `sessionIdType: 'customKey'` you bind the key to any expression (`{{ $json.sessionId }}`, a Slack `thread_ts`, a multi-tenant composite). Each user/thread/context gets its own bucket.
|
|
|
|
### `memoryPostgresChat` / `memoryRedisChat`
|
|
|
|
Reach for these only when memory must be queried or read **outside** the agent: displaying conversation history in your own UI, analytics on past chats, sharing memory across systems, or migrating instances cleanly.
|
|
|
|
```json
|
|
{
|
|
"parameters": {
|
|
"sessionIdType": "customKey",
|
|
"sessionKey": "={{ $json.sessionId }}"
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
|
|
"typeVersion": 1.3,
|
|
"name": "Postgres Memory"
|
|
}
|
|
```
|
|
|
|
**Wrong for** the default chat case — `memoryBufferWindow` already survives across executions and is the cleaner pick.
|
|
|
|
---
|
|
|
|
## Custom patterns (Chat Memory Manager)
|
|
|
|
Most agents don't need this. But when a fixed window isn't enough, the `@n8n/n8n-nodes-langchain.memoryManager` node operates against any wired memory backend and exposes three modes:
|
|
|
|
- **`load`** (default) — read current memory into the workflow (for inspection, branching on size, feeding a summarizer).
|
|
- **`insert`** — append a message. An optional `hideFromUI` flag covers messages that should affect the agent but not show in the chat UI.
|
|
- **`delete`** — remove some or all messages.
|
|
|
|
### Pattern: rolling summarization
|
|
|
|
When a conversation runs long and you want the gist of older turns instead of dropping them:
|
|
|
|
1. After each turn, `load` the buffer.
|
|
2. If it's approaching the cap, route to a summarizer (otherwise no-op).
|
|
3. Summarize the older turns with an LLM.
|
|
4. `delete` the buffer.
|
|
5. `insert` the summary as one message, plus the most recent few turns for continuity.
|
|
|
|
The agent now sees `[summary of turns 1-40] + [recent 5 turns]`, paying far fewer input tokens while keeping long-history context.
|
|
|
|
Other patterns built the same way: **prune by relevance** (`load` → filter → `delete` → `insert` the keepers), **inject runtime facts** (`insert` with `hideFromUI: true`), **reset on command** (`delete` all on `/clear`).
|
|
|
|
The Memory Manager node is more recent than the rest of n8n's memory tooling — verify the modes against your installed version before relying on them in production.
|
|
|
|
---
|
|
|
|
## Session ID handling by trigger
|
|
|
|
### Chat Trigger
|
|
Sets `sessionId` automatically. Wire it everywhere consistently:
|
|
- Memory: `sessionKey: ={{ $('Chat Trigger').first().json.sessionId }}`
|
|
- Tools: `sessionId: ={{ $('Chat Trigger').first().json.sessionId }}` (**NOT** through `$fromAI`)
|
|
- Storage keying: derive bucket keys / filenames from `sessionId` for trivial per-session cleanup.
|
|
|
|
### Webhook trigger
|
|
You manage it: the caller passes a header or body field (`body.sessionId`) and you forward it, or you issue one on first call and expect it back. Either way, it must be consistent across the whole conversation, including reconnections.
|
|
|
|
### Manual / scheduled
|
|
Usually no session. Use a stable identifier per "conversation" if one exists (ticket ID, thread ID); otherwise memory adds nothing — omit it.
|
|
|
|
---
|
|
|
|
## Memory and tools
|
|
|
|
When a tool is invoked, the tool's sub-workflow does **NOT** see conversation memory — memory is the agent's context, not the tool's input. Pass needed context through `$fromAI` parameters explicitly. For session-keyed state, plumb `sessionId` and have the tool look up state from a Data Table or storage keyed by session.
|
|
|
|
---
|
|
|
|
## Memory and binary
|
|
|
|
Memory stores **text turns**. Binary uploaded mid-conversation is NOT in memory — it's in the Chat Trigger's `files[]` for that turn only. The text memory captures that "the user mentioned uploading a file," but to actually use the file in a later tool call it must still be in storage and its key must be in **that** turn's system prompt. In practice, inject the session's file inventory into the system prompt every turn (loaded by `sessionId`). → **n8n-binary-and-data**.
|
|
|
|
---
|
|
|
|
## Common mistakes
|
|
|
|
- **Hardcoding `sessionId: 'default'`** — all conversations share one bucket; memory becomes meaningless.
|
|
- **Different `sessionId` on memory vs tools** — memory looks right but tools can't find related state.
|
|
- **Unbounded `memoryBuffer` for chat** — token cost grows until timeout. Use BufferWindow with a sane limit.
|
|
- **Adding memory where there's no session** — a "summarize this article" workflow doesn't need it.
|
|
- **Expecting tools to see memory** — they see only their `$fromAI` parameters and plumbed context.
|
|
- **Drift between the surface and memory** — if anything posts to the conversation outside the agent (a scheduled reply, a human writing directly), the agent operates on an incomplete view and will contradict messages it can't see. Whatever shows on the user-facing surface must also be `insert`ed into memory.
|
|
|
|
---
|
|
|
|
## Operational notes
|
|
|
|
- **Memory size drives token cost.** A 15-turn buffer of 200-token messages is 3000 tokens of input every turn before the user even speaks. Plan for it.
|
|
- **Rate limits.** A model that hits a limit fails mid-conversation; memory holds everything until then, and the next turn resumes (assuming session-id continuity).
|
|
- **Concurrent sessions.** Persistent backends key on `sessionId`, so concurrent conversations don't interfere. Verify with two simultaneous tests.
|
|
|
|
---
|
|
|
|
## Cross-references
|
|
|
|
- Where the agent fits → parent **SKILL.md**
|
|
- Passing session-keyed state into tools → **SUBWORKFLOW_AS_TOOL.md**
|
|
- Threading-as-session on chat surfaces → **CHAT_AGENT_PATTERNS.md**
|
|
- Session-keyed file storage → **n8n-binary-and-data**
|