Make it remember you.It forgets everything.
Prove it forgets.
New here: no new code. We replay the agent.py you ended lesson 2 with, to see the problem.
Two quick questions from last time. Guessing first helps it stick, even when it feels harder.
A real run on our PC (qwen3:8b), recorded . Your own run will word the answers differently.
It did not know the second time, and it did not know the third time either. Ana was said one message ago.
It is a morgue attendant. Names are not the point. My mother forgets mine too, and she has no such excuse.Prof. Graves
The head keeps nothing.
New here: no new code. This step shows why step 1 happened.
Each call is a fresh trip. The head doesn't keep your earlier message. It only sees what is in the list you send right then. In the code, that list is called "messages".
So the memory has to live in your code. Keep every message in a list, and send the whole list every time.
It reads, answers and forgets. My mother calls that a good listener.Prof. Graves
Keep a list outside the loop.
New here: a list kept outside the loop, so it survives from one message to the next.
Three changes to agent.py. The list is called messages in the code. The loop is while True: it repeats the lines under it for every message you type. Each box shows the whole file; only the yellow lines are new, and the rest is lesson 2's file. The drawing on the right builds the list as you read each change.
First, create the list once, above the loop, with the system message already inside it.
import json
import urllib.error
import urllib.request
SYSTEM = "You are a burned-out morgue attendant. Answer in one grumpy sentence."
messages = [{"role": "system", "content": SYSTEM}]
while True:
prompt = input("You: ")
if prompt == "quit":
break
body = {
"model": "qwen3:8b",
"messages": [{"role": "system", "content": SYSTEM}, {"role": "user", "content": prompt}],
"stream": False,
"think": False,
}
URL = "http://localhost:11434/api/chat"
note = json.dumps(body).encode()
request = urllib.request.Request(URL, note, {"Content-Type": "application/json"})
try:
with urllib.request.urlopen(request) as response:
answer = json.load(response)
except urllib.error.URLError as error:
raise SystemExit(f"Could not reach Ollama ({error}). "
"If it is not running, open another terminal and run: ollama serve")
print(answer["message"]["content"])messages = [ ... ]messagesis a variable. Square brackets make a list: things in order that you can add to later.{"role": "system", "content": SYSTEM}- Curly brackets make a dictionary: labelled values, like a JavaScript object.
rolesays who is speaking andcontentsays what they said.SYSTEMis the text from lesson 2 that sets the character. - position 0
- Python counts from 0, so the first item in a list sits at position 0.
- the old list in
body - Further down,
bodystill builds its own one-message list. Change 3 removes it. "stream": Falseand"think": False- Lesson 1's settings: one whole reply, no private notes. You never change them.
note- In the code,
noteis lesson 1's request on its way to Ollama. It is not one of the notes in the drawing, which are messages. - above the loop
- This line runs once. Inside
while Trueit would start a fresh list every time round, and the memory would vanish again.
Second, inside the loop, add what you typed to the list before you send.
import json
import urllib.error
import urllib.request
SYSTEM = "You are a burned-out morgue attendant. Answer in one grumpy sentence."
messages = [{"role": "system", "content": SYSTEM}]
while True:
prompt = input("You: ")
if prompt == "quit":
break
messages.append({"role": "user", "content": prompt})
body = {
"model": "qwen3:8b",
"messages": [{"role": "system", "content": SYSTEM}, {"role": "user", "content": prompt}],
"stream": False,
"think": False,
}
URL = "http://localhost:11434/api/chat"
note = json.dumps(body).encode()
request = urllib.request.Request(URL, note, {"Content-Type": "application/json"})
try:
with urllib.request.urlopen(request) as response:
answer = json.load(response)
except urllib.error.URLError as error:
raise SystemExit(f"Could not reach Ollama ({error}). "
"If it is not running, open another terminal and run: ollama serve")
print(answer["message"]["content"])messages.append(x)- Adds
xto the end of the list. It ispushin JavaScript. "role": "user"- This note is yours, not the head's.
"content": promptpromptholds the text you typed withinput().- before you send
- The new note goes in first, so the head sees your question as the last thing in the list.
Third, in body, send the whole list instead of a fresh one.
import json
import urllib.error
import urllib.request
SYSTEM = "You are a burned-out morgue attendant. Answer in one grumpy sentence."
messages = [{"role": "system", "content": SYSTEM}]
while True:
prompt = input("You: ")
if prompt == "quit":
break
messages.append({"role": "user", "content": prompt})
body = {
"model": "qwen3:8b",
"messages": messages,
"stream": False,
"think": False,
}
URL = "http://localhost:11434/api/chat"
note = json.dumps(body).encode()
request = urllib.request.Request(URL, note, {"Content-Type": "application/json"})
try:
with urllib.request.urlopen(request) as response:
answer = json.load(response)
except urllib.error.URLError as error:
raise SystemExit(f"Could not reach Ollama ({error}). "
"If it is not running, open another terminal and run: ollama serve")
print(answer["message"]["content"])- the old one-note list in
bodyis gone - Before,
bodybuilt a fresh list with the system note and only your newest message. Now it usesmessages. "messages": messages,- Inside the dictionary
body, the key"messages"now holds your whole list. Before, it held a brand new list with one message. - the trailing comma
- It separates this entry from the next one,
"stream".
You: My name is Ana. Ugh, another soul to file away—just another name to add to the list of people who didn't make it past the night. You: What is my name? Your name is Ana, and I'm so tired of this conversation. You: quit
It remembers. Your code did the remembering and the head just read it. (A real run on our PC.)
Congratulations. The breakthrough is a list. Welcome to AI engineering.Prof. Graves
Keep its words too.
New here: the head's own answers go into the list too.
Every note in the list has a role, a label for who said it. system is the standing orders from lesson 2, user is you, and assistant is the head's own earlier replies. So far the list holds only the first two. After the print line, add the replies too. This is the whole of agent.py at the end of lesson 3; only the yellow line is new.
import json
import urllib.error
import urllib.request
SYSTEM = "You are a burned-out morgue attendant. Answer in one grumpy sentence."
messages = [{"role": "system", "content": SYSTEM}]
while True:
prompt = input("You: ")
if prompt == "quit":
break
messages.append({"role": "user", "content": prompt})
body = {
"model": "qwen3:8b",
"messages": messages,
"stream": False,
"think": False,
}
URL = "http://localhost:11434/api/chat"
note = json.dumps(body).encode()
request = urllib.request.Request(URL, note, {"Content-Type": "application/json"})
try:
with urllib.request.urlopen(request) as response:
answer = json.load(response)
except urllib.error.URLError as error:
raise SystemExit(f"Could not reach Ollama ({error}). "
"If it is not running, open another terminal and run: ollama serve")
print(answer["message"]["content"])
messages.append(answer["message"])
answer- The whole reply from Ollama, which
json.loadturned into a dictionary. answer["message"]- Square brackets with a key look up one value in a dictionary, like
answer.messagein JavaScript. This value is itself a small dictionary:{"role": "assistant", "content": "..."}. messages.append(...)- Adds that dictionary to the end of the list, right after your question. It is already in the right shape, with its role set to
"assistant", so nothing needs rebuilding. - after the
print - It runs at the end of the loop. Next time round, the list holds your question and the head's answer.
answer["message"]["content"]- The line above it, which prints the reply: two lookups in a row, first the message, then its text.
Talk to it
Press the button to send each message. Each press is one call. The jar shows what goes in on that call: your whole list, and nothing stays in the head between calls.
When all three are sent, tick the yellow box, no list (lesson 2's code). It runs the same three calls with nothing kept, like step 1.
The messages list in your code, right now:
A real run on our PC (qwen3:8b), recorded , replayed here.
It can now quote itself, though it summarised rather than repeated. It reads its old answers like any other part of the list.
What breaks with a list that skips its replies
This is a different case from the yellow box. Here the list keeps your messages but skips the head's replies, because the last line of agent.py is left out. Calls 1 and 2 ask the same questions as before, but this is a separate recorded run, so the head's words differ. Call 3 asks something new. It replays by itself.
A real run on our PC (qwen3:8b), recorded , replayed here.
It has a memory and uses it to be rude. Progress.Prof. Graves
The list only grows.
New here: no new code. This step shows what the list costs.
Make the list grow
Pretend each message you type is one more call to the head. It goes into a demo list exactly as step 3 does it. This demo counts characters, not tokens, as a rough stand-in: counting tokens needs the head's own tool, which comes in lesson 4. Add three messages and watch the numbers.
Your list grows with every message. My mother's list of grudges works the same way, and nobody has ever dared to trim it.Prof. Graves
That's lesson 3.
The head remembers nothing between messages. Your code gives it a memory by keeping the conversation in a list and sending the whole list every time.
That list grows with every message. What happens when it gets too big is lesson 4.
- Messages
- The list your code keeps and sends to the head on every call: the head's only memory.
- Conversation history
- The same list, seen as the whole chat so far.
- Role
- The label on each message that says who said it: system (the standing orders), user (you) or assistant (the head's own replies).
- Call
- One trip: your code sends the list, and the head answers.
- Token
- A piece of text, often part of a word; the head reads and writes text in tokens. Lesson 0's 112-word paragraph became 131 tokens. Lesson 4 is about how many fit.
Professor Graves is made up. Claude, an AI, writes and checks these lessons, and ran every line on this page before you did.
Lesson 4: run it out of memory on purpose →