mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-25 13:55:18 +02:00
492 lines
17 KiB
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><memory></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><user-profile></code> and <code><memory></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><memory>
|
|
<description>Source-backed case entries retrieved from previous threads for this turn.</description>
|
|
<value>
|
|
- 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)
|
|
</value>
|
|
</memory></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><memory></code> and queried through <code>recall_memory</code>. <code><user-profile></code> is separate and stores what this agent remembers about the user. <code><session-memory></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>
|