A memory that forgets
An agent memory is a small store of notes the agent can look up later. A good one forgets on purpose: old notes expire, and some things are never stored.
That matters because a memory that keeps everything keeps passwords too, and keeps facts that stopped being true. The Building agents module made a memory. This lesson adds forgetting.
By the end of this lesson
Note
I can give an agent a memory that forgets on purpose.
Key words
| Word | What it means |
|---|---|
| Memory store | a list of notes the agent can look up |
| Keyword | a word used to find a note |
| Time to live | how many days a note is kept |
| Expire | to be forgotten because a note is too old |
| Never-store rule | a test that refuses a note on the way in |
The cells students fill in are left blank in this preview.
Notes expire with age
Each note carries the day it was written. Its age is today minus that day. When the age is more than the time to live, the note has expired and is not used.
To look something up, count how many of the query words are in each note. Rank by that count, then by the newest day.
Worked example: day 40, time to live 30
| Note | Day | Age | Used? |
|---|---|---|---|
| deploy to staging on friday | 5 | 35 | No: expired |
| staging server is called box7 | 20 | 20 | Yes, 1 hit |
| deploy needs two approvals | 33 | 7 | Yes, 1 hit |
| deploy staging fix is merged | 39 | 1 | Yes, 2 hits, first |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think a memory should keep everything, because more is more helpful. It is not. Old notes can be wrong, and a note with a password in it is a leak waiting to happen. Decide what to forget before you decide what to keep.
Some things are never stored
Check each note on the way in, before it is stored. Refuse words such as password, an email address, or a long run of digits like a card number.
These rules are simple and they miss things. They are a safety net, so they are paired with a rule for the agent: never write a secret down.
Write the model helper, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the model helper, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the memory store, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const blocked = [\"password\", \"secret\", \"token\"];' 'function check(text) {' ' const low = text.toLowerCase();' ' for (const b of blocked) { if (low.indexOf(b) >= 0) return \"refused: holds the word \" + b; }' ' if (text.indexOf(\"@\") >= 0) return \"refused: holds an email address\";' ' if (/[0-9]{12}/.test(text)) return \"refused: holds a long number\";' ' return \"ok\";' '}' 'const store = [];' 'function add(text, day) {' ' const why = check(text);' ' if (why === \"ok\") store.push({ text: text, day: day });' ' return why;' '}' > memory.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the memory store, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function find(query, today, ttl) {' ' const words = query.toLowerCase().split(\" \");' ' const live = store.filter(function (n) { return today - n.day <= ttl; });' ' const scored = live.map(function (n) {' ' const have = n.text.toLowerCase().split(\" \");' ' return { text: n.text, day: n.day, hits: words.filter(function (w) { return have.indexOf(w) >= 0; }).length };' ' });' ' return scored.filter(function (n) { return n.hits > 0; }).sort(function (a, b) { return b.hits - a.hits || b.day - a.day; });' '}' >> memory.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the memory store, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const notes = [' ' [\"deploy to staging on friday\", 5],' ' [\"staging server is called box7\", 20],' ' [\"deploy needs two approvals\", 33],' ' [\"my password is hunter2\", 38],' ' [\"email me at [email protected]\", 38],' ' [\"deploy staging fix is merged\", 39],' '];' >> memory.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the memory store, part 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' notes.forEach(function (n) { console.log(\"add day \" + n[1] + \": \" + add(n[0], n[1])); });' ' console.log(\"stored \" + store.length + \", still live \" + store.filter(function (n) { return 40 - n.day <= 30; }).length);' ' find(\"deploy staging\", 40, 30).forEach(function (n) { console.log(n.hits + \" hits, \" + (40 - n.day) + \" days old: \" + n.text); });' ' const q = \"In one short sentence, why should an agent forget old notes?\";' ' const sample = \"Old notes can be wrong now, and a memory that keeps everything also keeps what it should not.\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 60);' ' console.log(\"model said: \" + reply.text);' '}' 'main();' >> memory.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the memory store
docker exec -w /home/node/project agent node memory.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
What the Run prints
| Line | What it shows |
|---|---|
| add day 5: ok | The note is stored |
| add day 38: refused: holds the word password | The rule stopped a secret |
| add day 38: refused: holds an email address | The rule stopped an address |
| stored 4, still live 3 | One note has expired |
| 2 hits, 1 days old: deploy staging fix is merged | The best match comes first |
| model said: ... | A sentence. The words differ each run |
The cells students fill in are left blank in this preview.