Agent Memory
Try it yourself! This example is available as an executable Jupyter notebook.
This example shows how a KAOS agent uses a MemoryStore. When an agent is bound to a store, the runtime does two things automatically around every request: it recalls relevant memory and injects it into the model context before the run, and it persists the conversation after the run. On top of that automatic baseline, memory.tools optionally gives the model explicit save_memory / search_memory tools.
The agent here uses mock model responses, so the demonstration is deterministic and needs no live LLM. What we actually verify is the integration: after the agent handles a message, we query the memory service API directly and confirm the conversation was written into that session's window, then show that a second session has its own window.
Understanding the Flow
Every request flows through the same baseline: scoped recall before the run, then a level-less attributed write. Identity requirements derive from cluster posture.
Prerequisites
- KAOS operator installed (Installation Guide)
kaos-cliinstalled- Access to a Kubernetes cluster
Setup
import os
os.environ["NAMESPACE"] = "memory-example"mkdir -p ./tmp
kubectl create namespace "$NAMESPACE" 2>./tmp/null || true
kubectl config set-context --current --namespace="$NAMESPACE"Step 1: Deploy a Memory-Enabled Agent
We deploy the reusable memory sample: one ModelAPI, the local-mode support-memory store, and three agents with different read entitlements. This walkthrough uses the session-scoped session-assistant, which works without cluster user identity and uses a mock response so the example never calls the model.
The important part is the agent's config.memory block:
memoryStore: support-memorybinds all three agents to the store.user-assistantusesmaxReadScope: user;session-assistantusesmaxReadScope: session.agent-botstatesmaxReadScope: agentand exposes no explicit memory tools.- The store tunes its tiers with typed fields: a small
shortTerm.tokenBudgetmakes conversational compaction easy to exercise, andmediumTerm.enabledturns on the rolling digest that folds the overflow.
DEBUG_MOCK_RESPONSES makes the agent return a canned reply instead of calling the model, so the run is deterministic.
kaos samples deploy 7-memory-agent -n "$NAMESPACE"Step 2: Wait for the Store and the Agent
The MemoryStore comes up first; the agent's Deployment is only created once its store dependency is Ready.
for i in $(seq 1 90); do
phase=$(kubectl get memorystore/support-memory -n "$NAMESPACE" -o jsonpath='{.status.phase}' 2>./tmp/null)
[ "$phase" = "Ready" ] && break
sleep 2
done
echo "MemoryStore phase: $phase"
for i in $(seq 1 60); do
kubectl get deployment/agent-session-assistant -n "$NAMESPACE" >./tmp/null 2>&1 && break
sleep 2
done
kubectl wait --for=condition=available deployment/agent-session-assistant -n "$NAMESPACE" --timeout=180s
kaos agent tools session-assistant -n "$NAMESPACE" --jsonThe tool output shows the session-assistant has a session-only entitlement. On a cluster configured with user identity, run the same command for user-assistant to see search_memory.parameters_json_schema.properties.level.enum include session, agent, user, and group.
Step 3: Session 1 — Talk to the Agent
Send the agent a fact to remember. This is an ordinary chat request; the agent handles it and, because it is bound to the store, automatically persists the conversation afterwards:
AGENT_PORT=$(kubectl get svc/agent-session-assistant -n "$NAMESPACE" -o jsonpath='{.spec.ports[0].port}')
kubectl port-forward -n "$NAMESPACE" svc/agent-session-assistant "19001:$AGENT_PORT" \
> ./tmp/memory-agent-port-forward.log 2>&1 &
AGENT_PF=$!
sleep 4
curl --fail --retry 5 --retry-connrefused -s http://localhost:19001/v1/chat/completions \
-H 'content-type: application/json' \
-H 'x-principal: alice' \
-H 'x-session-id: memory-session-1' \
-d '{"messages": [{"role": "user", "content": "My favourite deployment port is 8080"}]}'
kill "$AGENT_PF" 2>./tmp/null || trueStep 4: Verify the Agent Wrote to the Store
Now we confirm the integration through kaos memory. With no query the command uses the list path, and the default --include all returns the same conversation's rolling summary and verbatim window:
kaos memory recall --store support-memory --scope session --session memory-session-1 -n "$NAMESPACE" --json > ./tmp/recall-session1.json
cat ./tmp/recall-session1.jsonimport json
recall = json.load(open("./tmp/recall-session1.json"))
recent = [tuple(pair) for pair in recall["short_term"]["window"]]
print("Stored turns:", recent)
assert ("user", "My favourite deployment port is 8080") in recent
print("SUCCESS: the agent persisted the conversation to the MemoryStore")Step 5: Session 2 — A Separate Conversational Window
Recall a different session directly. Its verbatim conversational window starts empty because conversational tiers are always session-keyed:
kaos memory recall --store support-memory --scope session --session memory-session-2 -n "$NAMESPACE" --json > ./tmp/recall-session2.jsonrecall = json.load(open("./tmp/recall-session2.json"))
recent = [tuple(pair) for pair in recall["short_term"]["window"]]
print("Session 2 window:", recent)
assert recent == []
print("SUCCESS: each session keeps an independent verbatim window")Enabling the Tools
Both assistants set tools: read. Memory always applies the automatic baseline (recall before a run, persist after) — that is what Steps 3–5 exercised. tools layers explicit, model-driven tools on top:
| Setting | Tools exposed | Arguments | The model can… |
|---|---|---|---|
| (unset) | none | — | rely purely on automatic recall/persist |
read | search_memory | query, required entitled level | look facts up at every level through maxReadScope |
write | save_memory | content | save a durable fact with server-derived attribution |
all | both | as above | save and search on demand |
search_memory accepts a validated read level but never owner values. save_memory accepts content only; principal, agent, and session attribution come from authenticated dependencies.
Scopes
Reads are partitioned by scope through maxReadScope; writes attach every verified contributor without a level:
session— one conversation only.store— the whole-store administrative view; unavailable to agent actor requests.user— all sessions for an authenticated principal.agent— only this specific agent.
Semantic Long-Term Recall (pgvector)
The steps above use the verbatim short-term tier, which needs no model. Genuine semantic long-term memory — where the agent distils facts with save_memory and recalls them by meaning rather than exact text — uses the embedding model and is best backed by pgvector.
Provision a development pgvector Postgres with the installer flag:
kaos system install --pgvector-memory-enabled --gateway-enabled --metallb-enabled --waitThen point the store at it by switching its storage block to external, referencing the generated kaos-memory-pgvector secret, and set the model references to a real provider:
spec:
storage:
type: external
external:
provider: pgvector
connectionSecretRef:
name: kaos-memory-pgvector
key: dsn
embeddingDims: 1536With a real embedder, a save_memory tool call (or automatic extraction) distils durable facts into the long-term vector store, and a later session recalls them semantically. This path is verified against a live cluster rather than in the model-independent checks above.
How It Works
- Automatic recall + persist — bound agents recall relevant memory before every run and persist the conversation after, with no code changes.
- Short-term window — recent turns are stored verbatim per session for conversational continuity; no model is needed.
- Long-term memory — the embedding model makes distilled facts semantically searchable;
tools: readlets the primaryuser-assistantsearch its entitled levels explicitly. - Scope — memory is isolated by scope, so sessions, users, and agents only see what they are entitled to.
- Storage —
localmode keeps everything in one pod (Chroma + SQLite on a PVC);externalmode uses pgvector for production and horizontal scaling.
Cleanup
rm -f ./tmp/recall-session1.json ./tmp/recall-session2.json
kubectl delete namespace "$NAMESPACE" --wait=false 2>./tmp/null || true