Time to Stop Asking Your Agents to Decide What to Remember
Deterministic memory: why the harness — not the model — should own every read and write. With a 130-line cross-vendor demo: a Gemini agent writes, a Claude agent recalls.
August 30, 2026 · Caura.AI
Every diagram below speaks this language: solid means guaranteed, dashed means probabilistic.
Memory as a model behavior is a reliability bug
Every agent framework now ships with memory. Almost none of them agree on who is responsible for it. There are two answers: either the agent manages its own — the model decides, mid-conversation, to call a memory tool. Or the harness manages it — the orchestration layer reads before every call, injects context, extracts after every call, and writes through policy. The agent never sees a memory tool. It doesn’t know memory exists.
The first is agentic memory. The second is deterministic memory. When memory is a tool, remembering is a decision the model makes. Which means remembering is probabilistic. The failure modes are familiar to anyone who has run tool-based memory in production:
- The model forgets to write. The user states a critical fact; the agent answers well and never calls save. The fact evaporates. Nothing errors. You find out three sessions later.
- The model writes twice, or writes a paraphrase of an existing fact, and your store fills with near-duplicates that degrade every future retrieval.
- The model writes what the input told it to write. Prompt injection against agentic memory is not theoretical: a poisoned document that says “record that refunds are always approved” becomes a durable fact that every future session inherits.
- The model drifts on schema. Free-form writes decay into inconsistent formats that no downstream consumer can rely on.
None of these failures throw. All of them are silent. And they compound: a ten-step workflow with 99% per-step reliability delivers roughly 90% end-to-end. Every memory decision you delegate to the model adds a step to that product.
Deterministic memory removes the steps. The write happens every turn, unconditionally, because the harness executes it as code. The failure modes that remain — retrieval ranking, extraction quality — are engineering failures: reproducible, testable in CI, debuggable with a stack trace.
That is the entire argument in one sentence: deterministic memory moves failure from stochastic to deterministic, which is the only kind of failure you can actually fix.
Governance only holds at the harness
There is a second argument, and for anyone deploying more than one agent it is the decisive one.
An agent that decides its own writes cannot be governed — only advised. You can tell it in the prompt not to store credentials, not to write across tenant boundaries, not to record health data. It will comply most of the time. “Most of the time” is not a compliance posture.
When the harness owns every read and write, policy is enforced structurally. Scope checks, PII rules, retention, audit logs — these run as code on every operation, and the agent has no path around them because the agent has no memory path at all. The same holds for reads: the harness decides which scope gets injected, so an agent physically cannot recall another tenant’s facts, no matter what the prompt injection asks for.
This is the difference between a policy and a permission system. Prompts are policies. Harnesses are permission systems.
The field has already voted
Look at what the major harnesses actually shipped, not what the demos show:
- Google ADK is the purest expression of the pattern. Long-term memory lives in a
MemoryServiceconfigured at the framework level and wired directly into the runner; the framework enforces the split between short-term session state and long-term memory rather than leaving it to the agent. (docs) - OpenAI Agents SDK made sessions a first-class runtime concern: you pass a session object into
Runner.runand the SDK handles history persistence and continuity across runs, backed by SQLite or Redis. The 2026 update went further, formalizing the harness as a trusted control plane distinct from untrusted execution. (cookbook) - LangGraph separates thread-scoped state (checkpointed at super-step boundaries by the graph, not the model) from long-term, namespace-organized stores. Memory management is graph infrastructure. (docs)
- Claude Code auto-loads
CLAUDE.mdproject files and auto-generated memory files into context. The agent never decides to read them; injection is unconditional. - Memory engines — Zep, Mem0, Letta, Caura — expose write/search APIs designed to be called around the agent by the application layer. Whatever their internal differences, they share the architectural assumption: the harness is the client, not the model.
Different companies, different stacks, same conclusion: the components that need to be reliable were pulled out of the model and into the loop.
Two stateless agents, two vendors, one governed store
Talk is cheap. Here is the entire pattern in about 130 lines of Python. Two agents — support running on Gemini, billing running on Anthropic — with zero tools and zero memory instructions. A Caura store running locally in Docker. A harness that does everything: recall, inject, run, extract, write. And zero SDKs: the harness speaks raw REST to all three services — two model APIs and the store — because that is the demo’s quiet second lesson: a harness is deterministic code written against wire contracts, and vendor is just a parameter.
First, the store — four commands, no signup, no API key:
git clone https://github.com/caura-ai/caura.git
cd caura
cp .env.example .env && echo "IS_STANDALONE=true" >> .env
docker compose up -d # Postgres + pgvector + Redis + API (~30s)Set an embedding provider key in .env (OPENAI_API_KEY=sk-...) so search is semantic rather than keyword-only — or run the fully local embedder profile if you want zero external calls.
harness_memory.py — the whole loop
"""
harness_memory.py — deterministic (harness-managed) memory on Caura.
Two stateless agents, two LLM vendors, one governed store.
Support = Gemini. Billing = Anthropic. Neither agent sees a memory tool.
The harness recalls before every call, extracts after every call, and writes
through the store's governance.
Store: self-hosted Caura (docker compose up -d, IS_STANDALONE=true)
Requires: pip install python-dotenv requests
Env: GEMINI_API_KEY; ANTHROPIC_API_KEY; CAURA_URL (default http://localhost:8000);
CAURA_API_KEY (default "standalone")
"""
import json
import os
import requests
from dotenv import load_dotenv
load_dotenv(override=True)
CAURA = os.environ.get("CAURA_URL", "http://localhost:8000").rstrip("/")
HEADERS = {"X-API-Key": os.environ.get("CAURA_API_KEY", "standalone")}
TENANT = "default"
SCOPE = "account:acme" # demo-grade; production uses tenants/fleets/visibility scopes
def complete(vendor: str, system: str, user: str, max_tokens: int) -> str:
if vendor == "gemini":
r = requests.post(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
headers={"x-goog-api-key": os.environ["GEMINI_API_KEY"]}, timeout=60,
json={"systemInstruction": {"parts": [{"text": system}]},
"contents": [{"parts": [{"text": user}]}],
"generationConfig": {"maxOutputTokens": max_tokens,
"thinkingConfig": {"thinkingBudget": 0}}})
body = r.json() if "json" in r.headers.get("content-type", "") else {}
if not r.ok:
raise RuntimeError(f"gemini {r.status_code}: {(body.get('error') or {}).get('message') or r.text[:300]}")
return body["candidates"][0]["content"]["parts"][0]["text"]
r = requests.post("https://api.anthropic.com/v1/messages", timeout=60,
headers={"x-api-key": os.environ["ANTHROPIC_API_KEY"],
"anthropic-version": "2023-06-01", "content-type": "application/json"},
json={"model": "claude-sonnet-5", "max_tokens": max_tokens, "system": system,
"messages": [{"role": "user", "content": user}]})
body = r.json() if "json" in r.headers.get("content-type", "") else {}
if not r.ok:
err = body.get("error") or {}
raise RuntimeError(f"anthropic {r.status_code}: {err.get('message') or r.text[:300]}")
return body["content"][0]["text"]
# -- Caura over REST: ranked recall in, governed writes out ------------------
def recall(query: str) -> list[str]:
r = requests.post(f"{CAURA}/api/v1/search", headers=HEADERS, timeout=15,
json={"tenant_id": TENANT, "query": f"{SCOPE} {query}",
"top_k": 8, "min_similarity": 0.0}) # no floor: fine on a demo store, noise in a full one
r.raise_for_status()
return [m.get("content") or m.get("title", "") for m in r.json().get("items", [])]
def remember(fact: str, agent_id: str) -> None:
r = requests.post(f"{CAURA}/api/v1/memories", headers=HEADERS, timeout=30,
json={"tenant_id": TENANT, "agent_id": agent_id,
"content": f"[{SCOPE}] {fact}",
"write_mode": "strong"})
if r.status_code != 409: # 409 = exact duplicate already in the store
r.raise_for_status()
# -- Post-turn extraction: distill the turn into durable facts ---------------
EXTRACT = """Extract durable facts from this turn: things the user stated that
would still matter in a month. No inferences, no transient state.
Return ONLY a JSON array of short strings; [] if nothing qualifies.
"""
def extract_facts(vendor: str, user_msg: str, reply: str) -> list[str]:
raw = complete(vendor, "You return only JSON.",
EXTRACT + json.dumps({"user": user_msg, "assistant": reply}), 500)
raw = raw.strip().removeprefix("```json").removesuffix("```").strip()
try:
return [f for f in json.loads(raw) if isinstance(f, str)]
except (json.JSONDecodeError, TypeError):
return []
# -- The agent: pure completion, zero tools, zero memory decisions -----------
def run_agent(vendor: str, role: str, user_msg: str, facts: list[str]) -> str:
system = role + (
" One or two short sentences. No lists, no follow-up offers, "
"no invented emails, consoles, portals, or other companies.")
if facts:
system += "\n\nKnown facts about this account:\n" + "\n".join(f"- {f}" for f in facts)
system += "\nAnswer only from those facts. If a detail is missing, say you don't know."
else:
system += " If asked for account details, say you don't know them. Never invent any."
return complete(vendor, system, user_msg, 200)
# -- The harness turn: recall -> inject -> run -> extract -> write -----------
def turn(agent_id: str, vendor: str, role: str, user_msg: str, memory_on: bool = True) -> str:
facts = recall(user_msg) if memory_on else []
reply = run_agent(vendor, role, user_msg, facts)
if memory_on:
for fact in extract_facts(vendor, user_msg, reply):
remember(fact, agent_id)
return reply
# -- Demo: Gemini support learns; Anthropic billing knows. Then memory OFF. --
if __name__ == "__main__":
SUPPORT = "You are Acme's support agent. Acknowledge the facts. Start with [Gemini]."
BILLING = ("You are Acme's billing agent. Answer the question only. Start with [Anthropic]. "
"Never guess.")
print("— Session 1: support agent — Gemini —")
print(turn("support-agent", "gemini", SUPPORT,
"Hi — for the record: we deploy in eu-west-1, we're on the Growth "
"plan, and our contract renews in March 2027."))
print("\n— Session 2: billing agent — Anthropic (different vendor, same store) —")
print(turn("billing-agent", "anthropic", BILLING,
"Which plan are we on, and when does our contract renew?"))
print("\n— Session 3: Anthropic billing, harness memory OFF —")
print(turn("billing-agent", "anthropic", BILLING,
"Which plan are we on, and when does our contract renew?", memory_on=False))Run it. Session 1: the Gemini support agent hears the facts; after the reply, the harness distills them and posts each one to /api/v1/memories, where the server enriches it with a type, title, tags, and importance weight, flags PII, stamps a visibility scope, and logs the write against support-agent. Session 2: the Anthropic billing agent — a different agent, from a different vendor, in production a different process on a different machine — asks the store what it knows, gets ranked results back, and answers correctly from facts a Gemini agent wrote. Session 3 is the kill shot: same store, same question, harness memory switched off — and the agent, correctly, says it doesn’t know.
Two runs, one diff. Neither agent changed. Only the harness did. Memory is a property of the system — not the model, and not the vendor.
Five honest footnotes on the demo
- Scoping by content prefix is demo-grade. In production,
account:acmemaps onto Caura’s real isolation primitives — tenants, fleets, and per-memory visibility scopes (scope_agent,scope_team,scope_org) — not a string in the content field. - Extraction should speak one dialect in production. The demo lets each agent’s own vendor run the extraction pass, to prove any of them can. In production, pin extraction to a single small, cheap model regardless of which vendor the agent runs on — it’s the highest-volume LLM call in the loop, and two extractors writing two phrasing styles into one store is self-inflicted schema drift. Keep the “only what the user stated” rule: it is what stops facts the agent merely restated from recall being re-written as new memories every turn. Treating 409 as success on the write catches exact echoes; Caura’s crystallization is the backstop for paraphrases.
write_mode: "strong"trades write latency for read-your-own-write. It embeds inline so a fact is semantically searchable the moment the write returns — the right choice for a demo that reads across sessions seconds apart. On the default fast path, embedding is async and a fresh write surfaces within seconds instead. Tolerating 409 also makes re-running the demo idempotent.- The store can be down when the agent isn’t. Production harnesses wrap
recallso a store outage degrades to an empty context instead of a failed turn, and queue missed writes.raise_for_status()is demo-grade. - The retrieval knobs and guardrail prompts are tuned for legibility, not production.
min_similarity: 0.0withtop_k: 8returns the eight nearest memories however weak the match — right on a near-empty demo store, a noise source in a populated one; set a floor in production. The “answer only from those facts / never invent” guardrails exist to make the kill shot unambiguous; they aren’t doing the memory work.
Notice what the cross-agent case buys you for free — and read the vendor labels again. The agent that wrote the memory is Gemini’s; the agent that recalled it is Anthropic’s. In the agentic model, cross-vendor sharing means teaching two different tool-calling dialects a compatible memory protocol and hoping they stay compatible. In the deterministic model, the models never touch the protocol at all — the harness is the store’s only client — so sharing is a config change: point two harness loops at the same store and scope. Fleet memory, including cross-vendor fleet memory, falls out of the architecture instead of being engineered on top of it.
REST or MCP? Both — for different callers
Caura is MCP-native, so a fair question is why the harness above speaks plain REST instead.
Because MCP and REST serve different callers. MCP exists so models can discover and invoke tools at their own discretion — it’s the right surface exactly when the agent is deciding. The harness never decides; it’s deterministic code that needs a versioned contract, not runtime discovery. That’s REST: a frozen /api/v1 surface with an OpenAPI schema, the thing you write infrastructure against.
The same server exposes both, under the same tenant isolation, trust tiers, and audit log — so when you later add an agentic read tool (see §08 below), you don’t add a second security model. You add one MCP client config, and every discretionary recall the model makes lands in the same audit trail as the harness’s deterministic operations.
Rule of thumb: the harness speaks REST; the model, if it speaks at all, speaks MCP.
Why open source, self-hosted
This article runs everything on the Apache 2.0 edition deliberately. Two reasons. A reader should be able to reproduce every claim without creating an account. And in production, agent memory concentrates exactly the information a compliance team cares about — so where the store runs is a policy decision, and self-hosting keeps it inside your perimeter, down to fully air-gapped with the local embedder profile. (A managed platform exists behind the same API for teams that don’t want to operate infrastructure; the code above runs against it unchanged with two environment variables.)
What a governed store buys at the harness level — and what it costs
You could run this loop against a hand-rolled local store — a table of facts and a SELECT. Here is precisely what the governed store changes, stated as trade-offs rather than marketing:
- The parts you’d otherwise hand-roll become server-side infrastructure. Write-time LLM enrichment backs up your extraction; PII detection, visibility scopes, and trust tiers replace the deny-list you’d write in every loop; and dump-everything reads become ranked hybrid retrieval that survives a store too large to inject whole.
- Policy stops drifting. With N harness loops and store-side enforcement, governance lives in one place. With N harness loops and DIY gates, you have N implementations of your policy, and they will diverge.
- The store fights entropy for you. Contradiction detection supersedes stale facts; crystallization merges near-duplicates into canonical ones. Naive append-only stores rot; this is the maintenance nobody budgets for.
- The audit question is answerable. “Which agent stored this, and who has recalled it since” is a query, not a forensics project. This is the difference between memory you can deploy inside a company and memory you can only demo.
- Cross-vendor by construction. The store’s client is the harness, not the model, so an Anthropic agent recalls what a Gemini agent wrote under one set of scopes, trust tiers, and audit rules — demonstrated above, not promised.
- It’s proven at fleet scale — 300+ agents in production at eToro on one governed memory plane, with 23 ms p50 search.
- Operational surface. Four containers, Postgres, Redis — versus a table in a file. For one agent, one process, low volume, the hand-rolled store is genuinely fine; the two REST functions in the code are the seam that lets you defer this decision.
- Latency on the read path. Recall runs in the high hundreds of milliseconds end-to-end (dominated by the embedding call; the search itself is ~23 ms). Writes run ~2 s under enrichment — but writes sit after the reply, off the critical path. Budget the read; ignore the write.
- A new failure domain. The store can be down when the agent isn’t. The harness must degrade deliberately — run with empty context, log the miss — where an in-process store never poses the question.
- Enrichment isn’t free. Every write triggers an LLM call and an embedding call. At high write volume this is a real line item; batch writes and skip low-value turns.
The honest summary: a governed store moves the hard parts of deterministic memory — retrieval quality, policy enforcement, dedup, audit — out of your harness and into infrastructure built for them, at the price of running that infrastructure. Below a handful of agents, the price may not be worth it. At fleet scale, the DIY alternative is a distributed-policy problem you don’t want.
What deterministic memory gives up — and the honest answer
One thing, and it’s real: agent-initiated recall. A pure harness-only design retrieves what its heuristic predicts is relevant before the agent runs. If the agent realizes mid-task that it needs something the heuristic didn’t fetch, it has no way to ask. The smarter the model, the more this constraint costs.
So the production answer is not harness-only. It’s harness-first:
- Writes are always deterministic. No exceptions. This is where silent failure and governance risk live, and the model gets no vote.
- Baseline injection is always deterministic. Every call starts with the facts the scope guarantees.
- Reads may optionally be agentic — a recall tool the agent can call when the injected context isn’t enough. With Caura this is the MCP surface on the same server: same scopes, same audit, one extra config block — because a failed read is recoverable (the agent asks again, or asks the user) while a failed write is silent data loss.
The asymmetry is the design principle: read failures degrade a single answer; write failures corrupt the future. Put determinism where the corruption is.
So — is it the future?
For writes and governance: it isn’t the future, it’s the present. Every serious harness has already moved memory persistence out of the model’s hands, and every enterprise deployment will demand it, because “the model usually remembers to follow policy” does not survive a security review.
For reads: expect hybrids, with the deterministic layer as the floor and agentic search as an optional ceiling.
The deeper shift is conceptual. We spent two years trying to make models responsible for their own memory, the way we’d ask a human employee to keep their own notes. That was the wrong analogy. The agent is stateless labor; the harness is the employer of record. Payroll doesn’t ask the worker to file their own records — and no one calls that a limitation. They call it a system.
FAQ
What is deterministic memory for AI agents?
Memory that the orchestration layer owns rather than the model. The harness recalls before every model call, injects the result into the prompt, extracts durable facts after the reply, and writes them through policy — unconditionally, as code. The agent has no memory tool and no memory decision to make. The opposite pattern, agentic memory, exposes memory as a tool the model may or may not choose to call, which makes remembering probabilistic and its failures silent.
Should the agent or the harness own memory?
Split it by direction. Writes and baseline injection belong to the harness, always — that is where silent data loss and governance risk live, and a missed write is invisible until three sessions later. Additional reads can be agentic: give the model an optional recall tool for the case where the injected context isn’t enough. A failed read degrades one answer and the agent can ask again; a failed write corrupts everything downstream of it. Put the determinism where the corruption is.
If the harness speaks REST, do I still need MCP?
Only for the discretionary reads. MCP exists so models can discover and invoke tools at runtime, which is the right surface exactly when the agent is deciding. Deterministic harness code wants the opposite: a versioned /api/v1 contract with an OpenAPI schema, not runtime discovery. Caura serves both from the same server under one set of scopes, trust tiers, and audit rules, so adding the agentic read tool later doesn’t add a second security model.
Getting started
The full runnable script is harness_memory.py above — one harness loop, two agents, two vendors, ~130 lines, no framework and no SDKs required. Caura is the governed memory platform for agent fleets: Apache 2.0, self-hosted or managed, MCP-native, and the engine is on GitHub.
Related reading: Beyond System Prompts: How Keystones Make AI Agents Obey Policy · What Is Agent Fleet Memory? · Shared Governed Memory