Skip to content

Memory Architecture

This page explains how KAOS memory works end to end — the tiers, the multi-tenant scope model, the service topology, and the control- and data-plane wiring — and the design choices behind each. For the concrete field references see the MemoryStore CRD, the Agent CRD memory block, and the runtime memory system. For a hands-on walkthrough see the Agent Memory example.

Overview

Memory in KAOS is augmentation, not a hard dependency: it enriches an agent's context but a memory outage degrades an agent rather than stopping it. An agent binds to a MemoryStore, and the operator deploys a single central memory service that every bound agent calls over the network. The service composes three tiers behind one HTTP contract, applies a server-derived tenancy scope to every operation, and persists long-term facts through the Mem0 engine embedded as a library.

Memory tiers

A remote-memory agent layers three tiers behind one client. KAOS owns the two conversational tiers as plain relational rows; the Mem0 engine owns the semantic long-term tier as a vector index.

TierScopeStorageOwnerPurpose
Short-termsession onlyrelational rows (SQLite/Postgres), no embeddingsKAOSverbatim recent-turn window replayed for conversational continuity; also the fallback when long-term is degraded
Medium-termsession onlyrelational rows, append-only + versionedKAOSa single rolling narrative digest per session that preserves continuity once older turns leave the window
Long-termcross-session, scope-keyedMem0 over a vector store (Chroma/pgvector)Mem0 enginesemantic + episodic facts extracted from turns and recalled by relevance

Design choices:

  • The short-term window is session-scoped only. User-, agent-, or store-wide verbatim windows are rejected because they would interleave concurrent conversations; cross-session continuity is served by long-term facts, not by merging live turns. The window is bounded by a configurable token budget (with a hard event-count safety cap), not by turn count.
  • The medium-term digest stays out of Mem0. Mem0 decomposes input into atomic, individually revisable facts for vector retrieval, whereas a rolling digest is a coherent narrative that should be injected directly at recall time. Indexing it into Mem0 would shred narrative continuity into fragments and pollute vector search, so the digest is a relational, append-only, versioned row and Mem0 receives only the raw evicted turns.
  • Short-term is never on Redis and never in Mem0. It is a cheap append-and-read-window operation co-located with the long-term store (a SQLite table beside embedded Chroma in local mode; a plain table on the same Postgres that backs pgvector in external mode).
  • Folding and extraction are always off the write path. The active window is computed lazily on read; when a compaction trigger is crossed, older turns fold into the digest and the raw turns are handed to Mem0 for long-term extraction — all as background work, never blocking the response.

Temporal (bi-temporal validity) and procedural (skill) memory are deferred as later capability tiers behind a future graph/temporal engine; the committed set is short-term plus a unified semantic-and-episodic long-term store.

Multi-tenancy: the scope model

Every operation carries a scope that decides whose memory is read or written. The scope is a single scope value on the Agent memory block, and the service maps it onto exactly one owner key — a flat owner-key model, not a nested path.

scopeOwner key appliedIsolation boundary
private (default)agent_id = <agent identity>the single agent (its own kaos://agent/<ns>/<name> identity)
useruser_id = <principal>every agent serving the same user principal
sharedagent_id = "kaos:shared" (reserved constant)every agent on the same MemoryStore
sessionrun_id = <session id>a single conversation/run

private and shared both use the agent_id key: the difference is the valueprivate uses the agent's own unique identity, while shared uses one reserved sentinel (kaos:shared) shared by all agents, deliberately distinct from any real agent identity so a shared write can never collide with an agent's private partition.

Design choices:

  • The store is the group. A logical group is the set of agents bound to the same MemoryStore; there is no separate group CRD. The four scope levels already express agent-private, per-user, fleet-shared, and per-session memory.
  • Isolation strength is chosen by how many stores you deploy. The default is a shared store with scope filtering; deploying one MemoryStore per tenant gives physical isolation (data is not co-located), so a filtering defect cannot leak across tenants. No isolation-mode field exists.
  • Scope governs only the long-term tier. The verbatim short-term window and medium-term digest are always session-scoped; scope: user does not create a user-wide conversational window.
  • Enforcement is fail-closed at the service. Scope is derived server-side from the authenticated agent identity and request context — never trusted from model- or tool-supplied arguments. An operation that cannot resolve a usable owner key fails rather than querying an unscoped store. Because the vector providers pre-filter during the query, a tenant's relevant memories are never dropped by an unfiltered nearest-neighbour window.
  • Erasure fans out synchronously across tiers. Right-to-erasure deletes the session short-term rows, the medium-term summaries, and the scope-filtered Mem0 long-term facts in one pass.

Cross-agent (A2A) delegation scope propagation and admin cross-user erasure depend on per-request principal propagation from the identity track and are deferred.

Deployment topology

  • One central service per store. The long-term engine runs as a single KAOS-owned service that imports Mem0 as a library (not the stock Mem0 server, which provides none of the tiering, scope injection, or telemetry). Embedding the engine in each agent was rejected — it would push extraction onto the serving process, multiply datastore connections, bloat every agent image, and diverge memory across replicas.
  • Packaged as kaos-memory. The wire contract, the MemoryServiceClient, and the service ship as one library layered behind extras: the core carries the contract and client; [service] adds Mem0, the vector store, and the FastAPI service; [pydantic-ai] adds the message adapters, server-side scope derivation, and the memory toolset. Client and server import the one contract, so they cannot drift.
  • Two storage modes, both tiers together:
    • local — everything in one container (embedded Chroma + a SQLite short-term table) on one PersistentVolume. Least-effort on-ramp; pinned to a single replica because the embedded store is a single-writer file.
    • external — pgvector for long-term and a plain table for short-term on the same Postgres. The service is stateless and the production path.
  • High availability (external). Because all durable state is the shared Postgres, external stores default to two replicas behind a plain Service with no sticky sessions, guarded by a PodDisruptionBudget (minAvailable=1). Consolidation is serialized through database-owned fold/flush work, so many replicas write and fold the same session without lost or double folds. Mem0's node-local history is not memory data and is disabled/ignored, so it does not impede scaling.
  • Background extraction is in-process, fire-and-forget. Long-term extraction, folding, and forgetting run off the response path in a bounded executor with bounded retry and a graceful drain on shutdown. There is no durable job queue in this version: the short-term tier is the durable path, and a durable at-least-once queue is a recorded follow-up to build only if crash-durability of extraction becomes a hard requirement.
  • Bind, do not operate. The operator deploys the service (Deployment, Service, and a PVC in local mode) but does not operate the external Postgres — it is bring-your-own via a connection secret. For a turnkey dev on-ramp, kaos system install --pgvector-memory-enabled provisions an opt-in development Postgres and a connection secret, never as a production default.

Control plane

The MemoryStore CRD describes infrastructure, model bindings, and two store-level knobs (extraction concurrency and the store-wide default failure mode). It deliberately does not carry the shared-window, digest, or sweeper marks — those are service-side configuration with env defaults, because they govern a window the service owns and are not per-agent policy. The two model roles reference an existing ModelAPI: summarization drives both Mem0's extraction prompt and the medium-term digest; embedding drives the vector index.

The Agent memory block selects a store by name and carries the agent-specific policy — scope, tools, failureMode, and the two forwarded clientParams (tokenBudget, rollingSummary). Enabling memory applies an automatic baseline unconditionally (recall-and-inject before each run, flush-for-extraction after); the tools knob layers additive explicit tools on top (readsearch_memory, writesave_memory, all → both).

Binding is fail-closed and degradation-aware:

  • type: remote requires a memoryStore; type: local forbids one. user/shared scope and any tools require a memoryStore (a pod-local store cannot serve cross-agent scopes or long-term tools), and are rejected rather than silently degraded.
  • A private owner is resolved from the injected AGENT_IDENTITY (kaos://agent/<ns>/<name>), never a name-only or empty owner, so identity-less agents cannot collapse onto one shared private partition.
  • Already-running agents treat a missing/not-ready store as degraded: the operator surfaces a MemoryDegraded condition and keeps the pod Ready serving short-term-only — a store outage never removes a serving agent. Only initial creation is gated: with waitForDependencies (default) the agent stays Waiting until the bound store is Ready, so it never starts up degraded.

Data plane (runtime)

In the agent runtime, RemoteMemory is a thin adapter over the kaos-memory MemoryServiceClient — it speaks the KAOS tiered contract and is not a Mem0 client. It bridges KAOS turns to Pydantic AI message history for full-fidelity replay of a continuing run, injects the assembled memory block (short-term window + medium-term digest + recalled long-term facts) before a run, and flushes the run's turns afterwards. The failure-mode contract is honoured across the client/service boundary: recall is always soft (a recall failure returns short-term-only context and never fails the turn), while write and forget re-raise only under failureMode: strict.

Design rationale at a glance

DecisionWhy
Central service, Mem0 as a librarythin agents, isolated extraction, cross-agent sharing, one datastore connection pool
KAOS owns short/medium-term relationallycheap append-and-read; a narrative digest must not be shredded into vector fragments
Flat owner-key scope, four valuesmaps cleanly onto the engine's native owner filters; the store is the group, no extra CRD
Server-side, fail-closed scopescope is non-spoofable and an unresolved scope never widens to an unscoped query
Store-per-tenant for physical isolationisolation strength is a deployment choice, not a code path
Fire-and-forget extraction, no queueLLM-dominated turn latency makes the async hop marginal; durability follow-up built only when measured
Memory is augmentationan outage degrades, never stops, an agent

Deferred capabilities

Recorded as forward-looking, out of the current critical path: temporal (bi-temporal) and procedural memory tiers; a durable at-least-once extraction queue; a Prometheus /metrics endpoint (health/readiness probes and a failure counter cover alpha operability today); dynamic cross-cutting agent groups beyond store membership; per-tenant quotas, logical export/import, and application-level field encryption; and a second long-term engine behind the memory interface.

Released under the Apache 2.0 License.