n8n/packages/@n8n/agents/docs/episodic-memory.html

492 lines
17 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Episodic memory</title>
<style>
:root {
color-scheme: light;
--color-primary: #ff6d5a;
--color-primary-soft: #fff1ef;
--color-text: #1f2937;
--color-text-light: #6b7280;
--color-text-xlight: #9ca3af;
--color-border: #d9dee8;
--color-border-light: #edf0f5;
--color-background: #f6f7fb;
--color-surface: #ffffff;
--color-code: #f3f5f8;
--shadow: 0 10px 30px rgba(31, 41, 55, 0.08);
--radius: 8px;
--mono: "SFMono-Regular", "Cascadia Mono", "Liberation Mono", Menlo, monospace;
--sans: Inter, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--color-background);
color: var(--color-text);
font-family: var(--sans);
font-size: 14px;
line-height: 1.55;
}
a { color: var(--color-primary); text-decoration: none; }
a:hover { text-decoration: underline; }
code, pre { font-family: var(--mono); }
code {
padding: 2px 5px;
border: 1px solid var(--color-border-light);
border-radius: 4px;
background: var(--color-code);
font-size: 0.9em;
}
pre {
margin: 0;
overflow: auto;
white-space: pre-wrap;
padding: 14px;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-code);
font-size: 12px;
line-height: 1.55;
}
.shell {
display: grid;
grid-template-columns: 248px minmax(0, 1fr);
gap: 24px;
width: min(1180px, calc(100vw - 32px));
margin: 0 auto;
padding: 32px 0 56px;
}
.sidebar {
position: sticky;
top: 24px;
align-self: start;
padding: 16px;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
box-shadow: var(--shadow);
}
.logo {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 18px;
font-weight: 700;
}
.logo-mark {
width: 20px;
height: 20px;
border-radius: 5px;
background: var(--color-primary);
}
.nav {
display: grid;
gap: 4px;
}
.nav a {
padding: 7px 8px;
border-radius: 6px;
color: var(--color-text-light);
font-size: 13px;
}
.nav a:hover {
background: var(--color-primary-soft);
color: var(--color-text);
text-decoration: none;
}
main {
display: grid;
gap: 18px;
min-width: 0;
}
.hero,
.card {
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
box-shadow: var(--shadow);
}
.hero { padding: 28px; }
.kicker {
margin: 0 0 10px;
color: var(--color-primary);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-size: clamp(32px, 5vw, 56px);
line-height: 1;
letter-spacing: 0;
}
.lede {
max-width: 760px;
margin: 16px 0 0;
color: var(--color-text-light);
font-size: 16px;
}
.card { padding: 22px; }
h2 {
margin: 0 0 12px;
font-size: 20px;
line-height: 1.25;
}
h3 {
margin: 0 0 8px;
font-size: 14px;
line-height: 1.35;
}
p { margin: 0 0 12px; color: var(--color-text-light); }
p:last-child { margin-bottom: 0; }
ul, ol { margin: 0; padding-left: 18px; color: var(--color-text-light); }
li + li { margin-top: 6px; }
.summary-grid,
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.detail-grid + .detail-grid { margin-top: 14px; }
.stat,
.block {
padding: 14px;
border: 1px solid var(--color-border-light);
border-radius: var(--radius);
background: #fbfcfe;
}
.stat strong,
.block strong {
display: block;
margin-bottom: 4px;
color: var(--color-text);
}
.stat span,
.block span { color: var(--color-text-light); }
.flow {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
margin-top: 8px;
}
.step {
position: relative;
padding: 14px;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: #fbfcfe;
}
.step::after {
content: "";
position: absolute;
top: 50%;
right: -9px;
width: 6px;
height: 6px;
border-top: 1.5px solid var(--color-text-xlight);
border-right: 1.5px solid var(--color-text-xlight);
transform: translateY(-50%) rotate(45deg);
}
.step:last-child::after { display: none; }
.step-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
margin-bottom: 10px;
border-radius: 50%;
background: var(--color-primary-soft);
color: var(--color-primary);
font-size: 12px;
font-weight: 700;
}
.callout {
padding: 14px;
border: 1px solid #ffd5ce;
border-radius: var(--radius);
background: var(--color-primary-soft);
color: var(--color-text);
}
.callout p { color: var(--color-text); }
@media (max-width: 1000px) {
.flow {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.step::after { display: none; }
}
@media (max-width: 900px) {
.shell {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
}
.summary-grid,
.detail-grid,
.flow {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="shell">
<aside class="sidebar" aria-label="Document navigation">
<div class="logo"><span class="logo-mark"></span> n8n agents</div>
<nav class="nav">
<a href="#overview">Overview</a>
<a href="#runtime">Runtime flow</a>
<a href="#entries">Entry shape</a>
<a href="#extraction">Extractor decisions</a>
<a href="#retrieval">Retrieval</a>
<a href="#config">Config</a>
<a href="#boundaries">Boundaries</a>
</nav>
</aside>
<main>
<header class="hero" id="overview">
<p class="kicker">Episodic memory</p>
<h1>Episodic memory</h1>
<p class="lede">
Episodic memory is the embedding-backed memory layer for source-backed entries from previous threads. It helps an agent recognize similar situations without mixing episodic evidence into the user profile or session memory.
</p>
</header>
<section class="card">
<div class="summary-grid">
<div class="stat">
<strong>Product name</strong>
<span>Episodic memory in the Agents Memory panel.</span>
</div>
<div class="stat">
<strong>SDK/config name</strong>
<span><code>Memory.episodicMemory(...)</code> and <code>memory.episodicMemory</code>.</span>
</div>
<div class="stat">
<strong>Storage</strong>
<span><code>agents_memory_entries</code>, scoped by <code>agentId + resourceId</code>.</span>
</div>
<div class="stat">
<strong>Requires</strong>
<span>An OpenAI credential in n8n. The n8n integration currently uses <code>openai/text-embedding-3-small</code>.</span>
</div>
</div>
<p>For implementation details and verbatim prompts, see <a href="episodic-memory-implementation.md">the episodic memory implementation notes</a>.</p>
</section>
<section class="card" id="runtime">
<h2>Runtime flow</h2>
<p>Episodic memory has a read path before the model call and a write path after successful turns.</p>
<div class="flow" aria-label="Episodic memory runtime flow">
<div class="step">
<span class="step-number">1</span>
<h3>Configure</h3>
<p>Enable <code>memory.episodicMemory</code> with an OpenAI credential.</p>
</div>
<div class="step">
<span class="step-number">2</span>
<h3>Retrieve</h3>
<p>Embed the current user message and search entries in the same <code>agentId + resourceId</code> scope.</p>
</div>
<div class="step">
<span class="step-number">3</span>
<h3>Inject</h3>
<p>Surface top entries in the model prompt as <code>&lt;memory&gt;</code>.</p>
</div>
<div class="step">
<span class="step-number">4</span>
<h3>Answer</h3>
<p>The model can use injected entries or call <code>recall_memory</code> for a deliberate lookup.</p>
</div>
<div class="step">
<span class="step-number">5</span>
<h3>Extract</h3>
<p>After the turn, extract, validate, dedupe, embed, and store new source-backed entries.</p>
</div>
</div>
</section>
<section class="card" id="entries">
<h2>Entry shape</h2>
<p>Stored entries are compact notes about concrete situations. They should preserve the useful mechanism, not just an isolated statement.</p>
<div class="detail-grid">
<div class="block">
<strong>What belongs here</strong>
<ul>
<li>Symptoms, environment details, and troubleshooting context.</li>
<li>Causal mappings and directionality, such as which record held state and which record was checked.</li>
<li>Mismatched identifiers or values, such as producer value versus matcher expectation.</li>
<li>Assistant diagnostic findings, confirmed resolutions, outcomes, and open case state.</li>
</ul>
</div>
<div class="block">
<strong>What stays out</strong>
<ul>
<li>Stable user preferences and profile-shaped context.</li>
<li>Agent behavior rules or rewritten agent instructions.</li>
<li>Current-thread objectives that only belong in session memory.</li>
<li>Generic advice, recalled-memory restatements, or unsupported speculation.</li>
</ul>
</div>
</div>
<pre>{
"entries": [
{
"content": "A workspace stayed inactive after renewal because record A held the active subscription while record B was used for entitlement checks. Merging the records and refreshing derived entitlements resolved the lockout.",
"source": "verified_assistant_finding",
"evidence": "The active subscription is on record A, but entitlement checks are reading record B."
}
]
}</pre>
</section>
<section class="card" id="extraction">
<h2>Extractor decisions</h2>
<p>The extractor reads the role-labeled transcript JSON after the agent has answered. It writes entries only when the transcript contains durable episodic context that could help a future turn recognize a similar situation, continue an investigation, avoid repeated work, or apply a previous mechanism or fix. Known <code>&lt;user-profile&gt;</code> and <code>&lt;memory&gt;</code> context can be passed in for dedupe and exclusions, but those blocks are not sources for new entries.</p>
<div class="detail-grid">
<div class="block">
<strong>When it stores an entry</strong>
<ul>
<li>The transcript contains a concrete situation with a useful mechanism, current diagnostic state, attempted step, ruled-out path, outcome, or open question.</li>
<li>The entry preserves causal direction, such as which record held state, which service checked it, or which emitted value did not match a rule.</li>
<li>Unresolved cases are allowed when the observation is stable enough to resume later. Uncertainty stays explicit instead of being upgraded into fact.</li>
<li>Useful details that only make sense together stay in one entry rather than being split into disconnected facts.</li>
</ul>
</div>
<div class="block">
<strong>When it skips</strong>
<ul>
<li>Stable user preferences, user-profile details, and agent behavior rules are not episodic entries.</li>
<li>Generic advice, assistant summaries, recalled-memory restatements, and unsupported recommendations are ignored.</li>
<li>Earlier diagnostic branches are skipped when the same transcript later corrects or supersedes them.</li>
<li>Current-thread details that have no likely value outside the thread are left to session memory.</li>
</ul>
</div>
</div>
<div class="detail-grid">
<div class="block">
<strong>Source labels</strong>
<ul>
<li><code>user_assertion</code>: the user directly stated the mechanism, fix, outcome, attempted step, or open state.</li>
<li><code>user_accepted_assistant_proposal</code>: the assistant proposed a mechanism or fix and the user explicitly accepted, applied, or verified it.</li>
<li><code>verified_assistant_finding</code>: the assistant stated a concrete diagnostic conclusion, ruled-out path, attempted-step result, or open case state.</li>
</ul>
</div>
<div class="block">
<strong>Evidence validation</strong>
<ul>
<li>Every default-extracted entry must include exact evidence from the transcript.</li>
<li>User-sourced entries require exact user-message evidence.</li>
<li><code>verified_assistant_finding</code> can use exact assistant evidence, or exact user evidence that confirms or grounds the finding.</li>
<li>After validation, entries are normalized, capped by <code>maxEntryLength</code>, limited by <code>maxEntriesPerTurn</code>, deduped by exact hash, checked for similarity duplicates, embedded, and stored.</li>
</ul>
</div>
</div>
</section>
<section class="card" id="retrieval">
<h2>Retrieval and injection</h2>
<p>The runtime retrieves broadly enough for the current turn, then injects entries most-recent-first so stale case context is less likely to dominate fresh context.</p>
<div class="detail-grid">
<div class="block">
<strong>Default retrieval</strong>
<ul>
<li><code>autoInject</code> defaults to <code>true</code>.</li>
<li><code>autoInjectTopK</code> defaults to <code>12</code>.</li>
<li>The current user message is the retrieval query.</li>
<li><code>recall_memory(query)</code> remains available for more specific lookups.</li>
</ul>
</div>
<div class="block">
<strong>Ranking and dedupe</strong>
<ul>
<li>Ranking uses lexical and vector signals plus recency weighting.</li>
<li>Exact normalized hashes prevent duplicate inserts.</li>
<li>Similarity dedupe defaults to <code>dedupeSimilarityThreshold = 0.86</code>.</li>
<li>Embedding vectors are never exposed to the frontend.</li>
</ul>
</div>
</div>
<pre>&lt;memory&gt;
&lt;description&gt;Source-backed case entries retrieved from previous threads for this turn.&lt;/description&gt;
&lt;value&gt;
- A priority item routed incorrectly because the source emitted tier=enterprise_plus while the matcher expected tier=enterprise-plus. Updating the matcher to accept both variants resolved the case. (2 days ago)
&lt;/value&gt;
&lt;/memory&gt;</pre>
</section>
<section class="card" id="config">
<h2>Config and storage</h2>
<div class="detail-grid">
<div class="block">
<strong>n8n JSON config</strong>
<pre>{
"memory": {
"enabled": true,
"storage": "n8n",
"episodicMemory": {
"enabled": true,
"credential": "openai-credential-id"
}
}
}</pre>
</div>
<div class="block">
<strong>SDK config</strong>
<pre>const memory = new Memory()
.storage(myMemoryBackend)
.episodicMemory({
embedder,
embeddingModel: "openai/text-embedding-3-small",
});</pre>
</div>
</div>
<ul>
<li><code>agents_memory_entries</code> stores content, content hash, provenance fields, embedding model name, embedding values, metadata, and timestamps.</li>
<li>n8n JSON config stores only the episodic memory credential. The integration maps that credential to <code>openai/text-embedding-3-small</code>.</li>
<li>SDK consumers pass a Vercel AI SDK <code>embedder</code>. <code>embeddingModel</code> is the stored label for that embedder, not a frontend setting.</li>
<li>n8n resolves embedding credentials through n8n credentials. Episodic memory does not read embedding credentials from environment variables.</li>
<li><code>semanticRecall</code> is a separate older recall path and is not part of Episodic memory.</li>
</ul>
</section>
<section class="card" id="boundaries">
<h2>Boundaries</h2>
<div class="callout">
<p>Episodic memory only powers source-backed entries injected through <code>&lt;memory&gt;</code> and queried through <code>recall_memory</code>. <code>&lt;user-profile&gt;</code> is separate and stores what this agent remembers about the user. <code>&lt;session-memory&gt;</code> is separate and stores current-thread state.</p>
</div>
<ul>
<li>Read and write scope is fixed to <code>agentId + resourceId</code>.</li>
<li>Episodic memory stores source-backed entries that may be useful in later threads.</li>
<li>User-profile memory stores what this agent remembers about the user.</li>
<li>Session memory stores the current thread objective, decisions, state, and follow-ups.</li>
</ul>
</section>
</main>
</div>
</body>
</html>