Governed Memory in agentic workflow: why I stopped dumping everything into the context window
A few weekends ago I pointed a local LLM at my own work. I'm a Customer Success Manager, and a good chunk of my job is just knowing the state of an account: who committed to what, when the renewal lands, what got said on the last call, what changed since the last one. That knowledge lives in dozens of call summaries, CRMs, a couple of spreadsheets, and a memory that does not scale. So the obvious move, the one every tutorial online shows you, is to dump all of it into a model and start asking questions.
I did exactly that. And the very first real question I asked, about a renewal figure, came back with the wrong number. Not wildly wrong. Just an old number from a call six months ago, delivered with total confidence and no hint that a newer, correct figure existed three tabs away in the workbook. The model was sure. I was the only one in the room who knew it was wrong.
That is the whole problem with the popular approach, and it took me one query to hit it. This post is about what I built instead, why I built it that way, and the handful of things that broke along the way. I approached it the way I'd approach an account problem: figure out the real requirement first, then design each piece and be ready to defend it.
Why the usual approach falls apart
The default recipe is retrieval-augmented generation with a big context window as the safety net. Grab everything that might be relevant, pad the prompt with it, and trust the model to sort it out. The promise is that context windows keep growing, so you can keep shoving more in and the problem takes care of itself.
It does not take care of itself. Here is what actually happens.
Three things go wrong at the same time, and they get worse the more data you add, which is the opposite of what you want. The provenance disappears. The answer is a blend of everything you retrieved, so when it comes out wrong you cannot tell which fact poisoned it, which means you cannot fix it either. The staleness is invisible. Last quarter's ARR and this quarter's ARR are both sitting in the prompt. The model picks one. It does not tell you which, and honestly it does not know which either. The noise drowns the signal. You over-fetch to be safe, and now the one chunk that actually mattered is competing with nineteen that did not. The frustrating part is that it fails in a way you cannot debug. It is not that the model cannot answer. It answers, confidently, and hands you no thread to pull. That is annoying on a hobby project. On a real account, in front of a real customer, it is the kind of thing that gets you in trouble.
What I decided to build instead
If the problem is that the system treats all facts as equally true and equally current, then the fix is to stop doing that. So the rule I landed on was: govern the memory, do not just accumulate it. Every fact the system holds carries four things. Where it came from. Which fact wins when two of them disagree. When it stops being true. And how much authority it has to override something else.
The job stops being "retrieve more" and becomes "know which version of a fact is true right now, and be able to explain why."
Let me make that concrete with the account I use for demos, a made-up retail customer I call Hearthaven. Say the workbook lists ARR at $4.2M, but a call summary from six months back mentions "the $3.8M renewal." The dump-everything approach puts both numbers in front of the model and crosses its fingers. Governed memory knows the workbook is the authority on financial facts, knows the call number is older and carries less weight, and answers $4.2M. And if you ask it why, it can actually tell you.
That is the whole idea. The rest of this post is the pieces I picked to make it real, walked one at a time, the same way I'd walk through parts on a build.
The pieces, and why I chose each one
Precedence, so the newest truth wins
The first decision was the one that would have saved me from that wrong renewal number on day one. When two facts disagree, I did not want the model averaging them into mush. I wanted something deterministic deciding which one wins, before the model ever lays eyes on it.
The order is fixed and you can inspect it. Account-specific beats global. A stated fact beats an inferred one. Newer beats older. The fact that loses does not get deleted, it gets shadowed, marked as superseded and kept around, so the history stays intact and I can always walk it back. The model reads a fact and tells me what it means. The resolver decides which fact is true. Keeping those two jobs separate is the whole trick, and the deciding half lives in plain code, not in a prompt.
Let the model read, let the code decide
That last point turned into the principle I lean on hardest, so it is worth stating on its own. Anything that has to be reliable lives in code. Which fact wins, whether it is still fresh, whether a number the model just wrote actually matches the source of truth. The model gets a narrow job: read messy human language and turn it into a clean, structured claim. After that, code takes over.
Here is what that buys you in practice. When the assistant states a hard fact, an ARR or a renewal date, a deterministic check verifies that number against the workbook before it ever reaches me. If it does not match, the answer gets flagged instead of sent. The model is allowed to be creative. It is not allowed to be the thing that certifies a number is right. That is not a job you hand to something probabilistic and hope for the best.
Scope retrieval to one account at a time
This one looks like a boring implementation detail and is actually the most important call in the whole build for anyone thinking about real deployment. Every query searches one account's data first, never a shared pool of everything.
The reason is simple and a little scary. If you let retrieval run across everything at once, one customer's facts can wander into another customer's answer. On a personal project that is a curiosity. In an enterprise, that is a data boundary being crossed, the sort of thing that ends a deal the moment security hears about it. Scoping the search to the account means the assistant physically cannot pull Hearthaven's numbers into someone else's answer, because that data was never in the pile it searched. Some things that look like quality problems are really security problems in disguise, and this is one of them.
Never let it learn something quietly
The last piece is about trust. The system never learns anything on its own in the background. A fact only enters memory through a deliberate act. And when the assistant does pull something out on its own, like guessing who committed to an action item on a call, that guess lands as a draft, not as truth. I confirm it, or I fix the one field that is wrong, and everything I leave alone counts as accepted. Once I have corrected something, that correction sticks through every future update.
The alternative is a system that quietly absorbs whatever it thinks it heard, and that kind of system rots. One bad guess becomes a "fact," which flavors the next guess, and the memory slowly fills with garbage nobody signed off on. Making every write deliberate, and putting me in the loop right at the uncertain spot, keeps the store clean and keeps my judgment above the model's.
A note on what the framework handed me, which was not much
I built this on standard agent-orchestration tooling, and it is worth being honest about the division of labor. The framework gives you the wiring: route a request through some steps, call the model, call a tool, pass the state along. The precedence resolver, the freshness logic, the provenance, the authority ranking, the draft-and-confirm loop, the fact-check against the workbook, none of that comes in the box. The framework connects the pieces. The governance is the part you have to design yourself. Which, honestly, is the whole point, because the interesting part of the product turned out to be exactly the part nobody hands you.
Why I run the model locally
I run the whole thing on my own hardware, on a local model, and it is tempting to file that under homelab tinkering. But in a work context it is the strongest point in the build, because it answers the first question anyone in security is going to ask.
When the model runs locally, the data, the model, and the reasoning all stay inside a boundary the customer controls. No account name, no contract figure, no call content leaves for some third-party API. For public sector and other regulated accounts, "where does the data go" is a question that needs a clean answer before anyone will talk about anything else. Running it locally gives you that answer. Same assistant, and suddenly a completely different conversation with the people whose job is to say no.
The things that broke, and what they taught me
The parts that worked were satisfying. The parts that broke were the ones that actually taught me something, and most write-ups skip this section, so here are the three worth keeping.
My tests passed and the thing still broke. I had a solid set of unit tests, all green, and then real bugs showed up only when the live model was running. The model would put an odd character in a date, or phrase a figure in a way my parser had never seen. My hand-written test cases never produced any of that, because when you write a test you unconsciously write the tidy version of the input. The lesson stuck: you have to test an agent against how the model actually behaves, not against the polite version of it you imagined.
You cannot make a guess deterministic by wanting it hard enough. I tried to automatically tag which side committed to each action item on a call. But the labels for who was speaking came from an upstream summarizer that guesses at them, they are not confirmed. Anything I built on top would just be confident nonsense stacked on a guess. So I stopped, and put myself in the loop right at that spot. The system drafts, I confirm. Know which of your inputs are solid and which are guesses, and do not try to paper over a guess with more model.
A false alarm hurts as much as a miss. At one point my fact-checker flagged a perfectly correct number as unverified, because a formatting mismatch made a right answer look wrong. That is every bit as bad as missing a real error, because a checker that cries wolf gets ignored, and an ignored checker protects nothing. In production, how well an agent judges its own confidence matters as much as whether it is right.
If there is one thread running through all three, it is this: make failure loud. Flag it, log it, announce it when the system degrades. Never let it quietly do the wrong thing. A silent failure is the one that actually hurts you, and a good chunk of the work on this project was just turning quiet failures into noisy ones.
Where I landed, and what is next
The thing I set out to fix, that confident wrong renewal number, does not happen anymore, because the system knows which figure is authoritative and can show its work. More than the specific fix though, building this changed how I think about these systems. The hard part of an AI agent in the real world is not getting it to answer. Models are good at answering. The hard part is getting it to fail safely, admit when it is unsure, and let a human correct it in a way that holds. That is governance, and it is engineering, not model size.
Which makes me think the thing that separates enterprise AI products going forward will not be whose model is biggest, because everyone will have a good enough model. It will be whose system you can actually trust: one you can audit, correct, keep scoped, and keep honest about what it does not know.
Next up is the eval layer. Right now I can tell you the system is built to be correct. I want to be able to measure how correct, on a fixed set of questions, so every change earns a before-and-after number instead of a hopeful feeling. Governance you cannot measure is governance you are only hoping for. I will write that one up once it is working.
If you are poking at similar problems, memory design, where the dump-it-all approach falls over, running models locally for data reasons, drop me a line. I always like comparing notes.