Every AI assistant you have ever used has the same hidden flaw. The moment you close the conversation, it forgets you completely. Your name, your preferences, the project you described in detail last Tuesday. Gone. You start over every single time. For a chatbot, that is annoying. For a business deploying AI workers, it is a fundamental architectural failure.
What This Agent Does
Nosis, the agent I built for this post, maintains two persistent memory systems that survive across sessions. The first is a core memory block, a small structured file of key facts: your name, your role, your company, your preferences. The agent reads this block at the start of every conversation and keeps it in active context the entire time. The second is an archival memory log, a growing timestamped record of every interaction. Before every reply, the agent searches that archive for anything relevant to what you just said and pulls it into the prompt automatically.
When the agent learns something new, it writes that fact to disk without being told to. When you restart the agent the next day, it already knows who you are. Over weeks and months, it builds a compounding knowledge base about you and your work. This is what Letta calls a memory-augmented agent.
Why I Built This
Every agent I built before this one had the same wall. You could have a good conversation, extract real value, reach genuine conclusions, and then close the terminal. Next session: blank slate. The agent had no idea you existed.
That wall is not a model quality problem. It is an architecture problem. The model is capable of remembering. The system surrounding it is designed to forget. Standard AI deployments treat every conversation as a disposable transaction. That is fine for a search engine.
What I discovered building this post is that memory is not a feature you add to an agent. It is a design decision you make before writing the first line. The Letta team understood this. They built an entire framework around the idea that an AI agent should manage its own memory the way a skilled professional manages their own notes: actively, selectively, and continuously.
What Happened
What surprised me: For this verification, I ran the agent through DeepSeek V4 Pro behind the local LiteLLM proxy. The important result was that the memory loop worked: the agent stored a long-term preference and recalled it on the next turn. The memory pattern is genuinely model-agnostic.
What changed during verification: During verification, I simplified agent.py from 231 lines to 176 lines and kept the same memory behavior. The dependency list was also corrected: the code imports the OpenAI SDK pointed at LiteLLM, so requirements.txt uses openai and python-dotenv, not the litellm Python package.
What I would change next: the archival memory search in this build works by matching keywords. If you tell the agent “I prefer concise explanations” and later ask about your communication style, the search looks for words like “communication” and “style” in the archive. If those exact words are not in the stored entry, it finds nothing, even though the stored fact is directly relevant. A production version would use something called embedding-based semantic search. An embedding is a way of converting any piece of text into a list of numbers that represents its meaning mathematically. Two sentences that mean the same thing produce similar numbers even if they share no words. The search then compares those numbers instead of comparing words, so “I prefer concise explanations” and “what is my communication style” would be recognized as related concepts and the right memory would be retrieved. This is what Letta uses PostgreSQL with the pgvector extension for in its full production stack. For this build, keyword search is sufficient to demonstrate the pattern. The semantic layer is the upgrade path when the agent starts handling thousands of memories and keyword matches become too imprecise.
Why this matters: the memory architecture can work across providers because it sits outside the model itself The model never touches the memory files directly. It only sees what the agent injects into the prompt.
The Architecture: How It Works
The Letta framework in its full form is a production platform with a FastAPI server, a PostgreSQL database with vector search extensions, Docker containerization, and a sandboxed code execution environment. That infrastructure is the right choice at enterprise scale. For this post I stripped it to the three components that carry the architectural lesson.
Core Memory
Core memory is a small JSON file containing structured facts about the user. Think of it as the agent’s permanent notepad, always open on the desk. It holds four fields: name, role, preference, and company. Every time the agent starts, it reads this file and places its contents directly into the system prompt. The model always knows who it is talking to.
The critical design decision here is that core memory is small on purpose. It stays in the active context window for the entire conversation. Large files would consume too much of the model’s attention budget. Core memory holds only the facts worth keeping forever and keeping close.
Archival Memory
Archival memory is a JSONL file, a plain text log where every line is a timestamped JSON entry. Every interaction is appended. Every memory update the agent decides to record is appended. The file grows continuously and is never truncated.
Before every reply, the agent searches this file for entries relevant to the current message. It pulls the most relevant entries into the prompt as context. The model sees its own history without the entire history consuming the context window. This is the core Letta insight: the agent manages what it looks at, rather than trying to look at everything at once.
The Memory Update Protocol
The agent is instructed in its system prompt to append a special prefix to its reply whenever it learns something worth remembering: MEMORY_UPDATE: followed by the fact. The agent code parses every reply for this prefix before showing the response to the user. If the prefix is found, the fact is written to archival memory, the prefix is stripped from the reply, and the user sees only the clean response.
This is how the agent writes its own memory without any additional infrastructure. The model decides what is worth remembering. The code handles the writing. The user sees nothing except the conversation.
The Build: Step by Step
The build has two files that matter architecturally: agent.py which runs the conversation loop, and memory_manager.py which handles all memory read and write operations.
Step 1: Separating Memory Logic from Agent Logic
The first design decision was to put all memory operations in a separate file. This is not required technically but it matters architecturally. When you read the agent code, the memory layer is invisible. When you read the memory manager, there is no agent logic. Each file has exactly one job.
from memory_manager import (
load_core_memory,
save_core_memory,
append_archival_memory,
search_archival_memory,
)
What This Code Is Actually Doing
This imports four functions from the memory manager. Each function name describes exactly what it does. Loading core memory reads the JSON file from disk. Saving core memory writes it back after an update. Appending archival memory adds a new timestamped entry to the log. Searching archival memory scans the log for entries relevant to the current message. By importing these four functions, the agent loop never touches files directly. All file operations are isolated in one place, which means if you ever want to replace the file system with a database, you change one file and nothing else breaks.
Step 2: Building the System Prompt with Memory Context
def build_system_prompt(config, core_memory, archival_entries):
core_block = "\n".join(
f"{key}: {value}" for key, value in core_memory.items() if value
) or "No facts stored yet."
archival_block = "\n---\n".join(archival_entries) if archival_entries else "None found."
return (
f"You are {config['agent_name']}, a memory-augmented AI assistant built by NosisTech LLC.\n"
"You have core memory for key user facts and archival memory for past interactions.\n\n"
"When you learn a new important fact about the user, append it to your reply "
"using this exact format on its own line:\n"
"MEMORY_UPDATE: [the fact to remember]\n\n"
"Only use MEMORY_UPDATE for genuinely new long-term facts. Do not repeat facts "
"already present in core memory.\n\n"
f"Core memory:\n{core_block}\n\n"
f"Relevant past interactions:\n{archival_block}"
)
What This Code Is Actually Doing
Every time the user sends a message, this function builds the system prompt fresh. It takes the current core memory dictionary and formats it as readable key-value pairs. It takes whatever archival entries the search returned and joins them with separators. Then it assembles the full system message: the agent’s identity, the instruction to use the MEMORY_UPDATE prefix when learning something new, the core memory block, and the relevant past interactions.
The model receives this as its instructions before seeing the user’s message. This is how the agent always knows who it is talking to and what it has learned before. The prompt is rebuilt from the current state of memory on every single turn. There is no caching. The agent always works from the latest version of what it knows.
Step 3: Parsing the Reply for Memory Updates
def parse_reply(reply_text):
visible_lines = []
updates = []
for line in reply_text.splitlines():
if line.startswith("MEMORY_UPDATE:"):
fact = line[len("MEMORY_UPDATE:"):].strip()
if fact:
updates.append(fact)
else:
visible_lines.append(line)
visible_reply = "\n".join(visible_lines).strip()
if not visible_reply and updates:
visible_reply = "Got it. I'll remember that."
return visible_reply, updates
What This Code Is Actually Doing
The fallback reply matters because some models return only the MEMORY_UPDATE line. Without the fallback, the user could see a blank response even though the memory was saved.
After the model replies, this function reads the reply line by line. Any line that starts with MEMORY_UPDATE: is pulled out and added to a list of facts to remember. Any line that does not start with that prefix is kept for the user to see. The function returns two things: the cleaned reply with all MEMORY_UPDATE lines removed, and the list of facts the agent decided to remember.
The user never sees the MEMORY_UPDATE lines. The agent uses them as a private channel to write to its own memory. This separation is what makes the memory system invisible from the user’s perspective while remaining fully transparent in the code.
Step 4: Updating Core Memory When a Fact Qualifies
def update_core_memory(fact, core_memory):
if ":" not in fact:
return False
key, value = fact.split(":", 1)
key = key.strip().lower()
value = value.strip()
if key in {"name", "role", "preference", "company"} and value:
core_memory[key] = value
return True
return False
What This Code Is Actually Doing
Not every fact belongs in core memory. Most facts go to the archival log. But when the agent learns your name, your role, your company, or a strong preference, that fact belongs in the always-available core memory block so it is never buried in the archive.
This function checks whether a new fact starts with one of those four structured keys. If it does, it updates the core memory dictionary in place and returns True so the agent knows to save the file. If the fact does not match any of the four fields, it goes only to the archive. This is the agent making a two-tier filing decision: important facts go in the top drawer, everything else goes in the archive.
Why This Matters
Every business that deploys AI assistants eventually hits the stateless ceiling. The assistant is useful in the moment but builds no value over time. Each conversation starts from zero. The compounding knowledge that makes a human employee more valuable after six months simply does not accumulate.
The memory architecture in this post is the foundation that changes that equation. An agent built on this pattern becomes more useful every day it runs. It learns the terminology your team uses. It remembers the decisions you made last month and why. It recalls that you prefer bullet points over paragraphs and that your company uses a specific approval process. None of that requires retraining the model. It requires only that the agent writes down what it learns and reads it back before every reply.
For a founder or operator, the practical implication is this: the difference between a stateless AI tool and a stateful AI worker is not the model. It is the memory layer around the model. That layer is inexpensive to prototype and compounds in value indefinitely.
Tools Used
- Letta (Apache 2.0): github.com/letta-ai/letta
- NosisTech Agent Engineering (MIT): github.com/nosistech/agent-engineering
(c) 2026 NosisTech LLC. Licensed under CC BY 4.0. Use freely, just credit us.