Every AI assistant you have ever used starts fresh. Open a new session and it has no idea who you are, what you discussed last week, or what problem you were trying to solve. For casual conversations that is fine. For a business running client relationships, compliance workflows, or operational processes on AI, that lack of continuity creates friction.
I built an agent that remembers past interactions by saving them to a persistent file. When a returning request arrives, it searches the saved history, finds the most relevant prior interaction, and injects that context into the next model response.
We will explore how episodic memory works in a software system and what it actually means for an AI agent to remember useful business context.
What This Agent Does
Most AI tools are stateless. Every conversation starts from zero. This agent is different. It maintains a running record of every interaction it handles, stored as a JSON file that survives across runs. When a new request arrives, the agent scans its history, finds the most relevant prior interaction based on word overlap, and uses that context to shape its response.
The result is an agent that behaves more like a staff member who was in the room for the last conversation. If the user tells it that Apex Dynamics prefers Friday status updates, the next request about Apex Dynamics can use that saved detail instead of forcing the user to repeat it.
That is the entire point of this build. It does not plan multi-step workflows. It does not score risk. It does not escalate to a human manager. Those are useful patterns, but they are not the lesson here. This version focuses on one architecture question: how does an agent remember something from a prior turn and use it later?
Why I Built This
The gap I kept running into was not capability. The AI models available today are extraordinarily capable. The gap was continuity. Every agent I had built up to this point forgot everything the moment the session ended. For a one-off task that is acceptable. For anything involving a multi-day project, client preference, internal process, or long-term relationship, it becomes a real limitation.
I wanted to see what happens when you give an agent a memory it can actually use. Most AI memory systems you will read about involve something called a vector database. When an AI reads a sentence, it can convert that sentence into a long list of numbers, called a vector, where the numbers represent the meaning of the words rather than the words themselves. Two sentences that mean the same thing but use completely different vocabulary can produce number lists that are mathematically close to each other. A vector database stores those number lists and can find close matches quickly.
A knowledge graph is a different approach. Instead of numbers, it stores facts as connected nodes. A client connects to a project. That project connects to a deadline. That deadline connects to a risk. The graph lets an AI navigate relationships between concepts rather than just matching words.
Both approaches matter at enterprise scale. Neither one is the right starting point for understanding memory. I built this agent with a JSON file and a word overlap function instead. Every interaction gets saved as a plain text record. When a new request arrives, the agent counts how many words it shares with each saved record and returns the best match.
That is it. No embeddings. No database. No graph traversal. No extra Python packages. Just a file, a retrieval function, and a prompt that gives the model the prior context when the agent finds it.
The discovery here is simple but important: memory does not need to start as infrastructure. It can start as a behavior. The agent hears something, saves it, looks for it later, and uses it when it matters.
The Architecture: How It Works
The agent has four core pieces that work together on every run.
The first piece is configuration. The agent reads environment-specific values from .env: the LiteLLM base URL, the model name, the API key, and the optional memory file path. That keeps the code model-agnostic. Switching providers happens through configuration instead of rewriting the Python file.
The second piece is the memory file. The agent stores prior interactions in memory.json. Each record contains a timestamp, the user input, and the agent response.
The third piece is retrieval. When a new request arrives, the agent turns the request into a set of lowercase words. It does the same thing for every saved memory. Then it counts how many words overlap. The saved interaction with the highest overlap becomes the relevant memory.
The fourth piece is prompt injection. Prompt injection here means the agent inserts the retrieved memory into the message list it sends to the model. The model receives the current user request plus a short system message containing the relevant previous interaction. The model can then answer with that context available.
This is memory in its simplest working form. Save the interaction. Retrieve the closest prior interaction. Put it into the next prompt. Save the new response.
The Build: Step by Step
Step 1: Environment setup. The agent uses a .env file for environment-specific configuration. The LiteLLM base URL, model name, API key, and optional memory file path live outside the code.
Step 2: Startup validation. Before making an API call, the agent checks that the required environment variables exist. If any are missing, it prints a clear error and exits.
Step 3: Memory path selection. The agent reads MEMORY_FILE_PATH from the environment. If no value is provided, it uses memory.json in the project folder.
def memory_path() -> str: path = os.getenv("MEMORY_FILE_PATH", "memory.json") if not os.path.isabs(path): path = os.path.join(PROJECT_DIR, path) return path
What This Code Is Actually Doing
This function decides where the agent will store memory. If the .env file gives a full path, the agent uses it. If the .env file gives a simple filename like memory.json, the agent places that file inside the project folder. That keeps the memory location configurable without hardcoding a machine-specific path into the code.
Step 4: Loading and saving memory.
def load_memory() -> list[dict[str, str]]: path = memory_path() if not os.path.exists(path): return [] with open(path, encoding="utf-8") as file: return json.load(file) def save_memory(records: list[dict[str, str]]) -> None: path = memory_path() with open(path, "w", encoding="utf-8") as file: json.dump(records, file, indent=2)
What This Code Is Actually Doing
load_memory checks whether the memory file exists. If it does not exist yet, the agent starts with an empty list. If it does exist, the agent opens the file and loads the saved interaction records.
save_memory takes the full list of records and writes it back to the memory file as formatted JSON. JSON is a plain-text data format that stores information in a structure Python can read later. In this build, that simple file is enough to prove the memory pattern.
Step 5: Turning text into comparable words.
def words(text: str) -> set[str]: return set(re.findall(r"[a-z0-9]+", text.lower()))
What This Code Is Actually Doing
This function takes a sentence and turns it into a set of lowercase words and numbers. A set is a Python collection that keeps one copy of each item. If the same word appears five times, the set only keeps it once. This gives the retrieval function a clean way to compare one request against another.
Step 6: Finding the most relevant memory.
def find_relevant_memory( user_input: str, records: list[dict[str, str]] ) -> dict[str, str] | None: query_words = words(user_input) best_record = None best_score = 0 for record in records: memory_text = record["user_input"] + " " + record["agent_response"] score = len(query_words & words(memory_text)) if score > best_score: best_score = score best_record = record if best_score == 0: return None return best_record
What This Code Is Actually Doing
This function compares the current request against every saved interaction. It combines the old user input and old agent response into one memory text, turns that memory text into a word set, and counts how many words overlap with the new request.
The & symbol is Python’s intersection operator. In plain English, it means “give me only the words that appear in both places.” The record with the highest overlap score becomes the memory the agent sends into the next prompt. If there is no overlap at all, the function returns nothing and the agent answers without prior context.
Step 7: Injecting memory into the model request.
def answer_with_memory(user_input: str) -> str: records = load_memory() relevant_memory = find_relevant_memory(user_input, records) if relevant_memory: memory_context = ( f"Relevant previous interaction:\n" f"User: {relevant_memory['user_input']}\n" f"Assistant: {relevant_memory['agent_response']}" ) else: memory_context = "No relevant previous interaction found." messages = [ { "role": "system", "content": ( "You are a practical business assistant for NosisTech LLC. " "Use the memory context when it is relevant. " "If the user states a preference or fact, acknowledge it clearly. " "Do not claim you completed external actions." ), }, {"role": "system", "content": memory_context}, {"role": "user", "content": user_input}, ] agent_response = call_llm(messages) records.append( { "timestamp": datetime.now().isoformat(timespec="seconds"), "user_input": user_input, "agent_response": agent_response, } ) save_memory(records) return agent_response
What This Code Is Actually Doing
This is the heart of the agent. It loads the saved memory, searches for the most relevant prior interaction, and turns that memory into a system message. A system message is an instruction or context block sent to the model before the user’s request.
If the agent finds a relevant memory, it sends the old user input and old assistant response into the prompt. If it does not find one, it tells the model that no relevant prior interaction was found. Then the model answers the new request.
After the model responds, the agent saves the new interaction into memory. That means every turn can become useful context for a later turn.
Step 8: Built-in memory demo. When the file runs without command-line input, it executes two turns.
def main() -> None: check_environment() if len(sys.argv) > 1: run_turn(" ".join(sys.argv[1:])) return run_turn("Remember that Apex Dynamics prefers Friday status updates.") run_turn("How should I schedule Apex Dynamics updates?")
What This Code Is Actually Doing
This gives the project a simple test path. The first turn stores a client preference. The second turn asks a related question. Because the words “Apex Dynamics” and “updates” overlap, the agent retrieves the first interaction and gives the model that memory before answering.
That small two-turn flow proves the architecture. The agent is not pretending to remember. It is reading a saved file, finding relevant history, and using it in the next response.
One practical issue showed up during testing on Windows: the model returned smart punctuation, but the script was forcing console output through ASCII. That made characters like em dashes and curly apostrophes print as question marks. I fixed that by configuring stdout and stderr for UTF-8 and printing the model response directly.
What I Would Do Differently
The memory retrieval in this build uses word overlap. That means two requests that are about the same topic but use different vocabulary may not match. A request about “encryption compliance” might not retrieve a prior record about “security certificate settings” even though both could be related in a real business conversation.
A production version would likely upgrade retrieval from word overlap to meaning-based search. That could mean embeddings, a vector database, or another retrieval method that compares concepts instead of exact words. The architecture already has a clean place for that upgrade: replace find_relevant_memory while leaving the rest of the agent intact.
The memory file also grows every time the agent handles a request. For a teaching build, that is fine. For a business system handling many client interactions, I would add retention rules, archiving, and access controls around the stored records. Memory creates leverage, but it also creates responsibility.
The save operation in this simplified version writes directly to the JSON file. For a stronger deployment, I would add safer persistence patterns, especially if the agent were writing high-volume or business-critical records. The current version keeps the code small so the memory pattern stays visible.
I would also add clearer memory boundaries. Not every prior interaction deserves to influence a future answer. A production agent needs rules for what gets remembered, what gets ignored, what expires, and what requires human review before reuse.
That is where the real consulting value lives. The code proves the mechanism. The business architecture decides what the agent is allowed to remember and how that memory gets governed.
What This Means
If you are running a services business, your team spends a measurable amount of time re-establishing context. A client calls with a follow-up question. Someone has to find the prior email thread, remember what was promised, and reconstruct the situation before giving an answer. That overhead compounds across every client relationship in your portfolio.
An agent with persistent memory reduces that friction for routine interactions. It can remember a preference, retrieve a prior exchange, and use that context when the next request arrives. Even this small implementation shows the business value immediately: continuity changes the quality of the response.
The bigger lesson is that memory is not one giant feature. It is a sequence of practical design choices. What gets saved? How is it retrieved? When is it injected into the prompt? How does the system avoid using irrelevant history? How does the business govern what the agent remembers?
This build answers the first version of those questions with the smallest working implementation I could make. It is not trying to be an enterprise memory platform. It is showing the foundation clearly enough that the next layer can be built with intent.
Tools Used
- LiteLLM (MIT): github.com/BerriAI/litellm
- NosisTech Agent Engineering — github.com/nosistech/agent-engineering
(c) 2026 NosisTech LLC. Licensed under CC BY 4.0. Use freely, just credit us.