Architecture

On this page

Package dependency graph

core has zero internal dependencies and is imported by every other subpackage. intelligent is the only subpackage that imports from every other subpackage — it is the integration layer.

Note that security (masking, encryption, access control) does not depend on core — it operates on plain strings/dicts and is intentionally reusable outside the memory system.

Memory tiers at a glance

AIMemoryManager.save_interaction sequence

AIMemoryManager.build_context sequence

Initialization / lazy construction

AIMemoryManager does not eagerly create per-tenant resources. _TenantBundle (which owns ShortTermMemory, WorkingMemory, LongTermMemory, SemanticMemory) is constructed on first access to a given tenant_id via _bundle():

Python · 4 lines
def _bundle(self, tenant_id: str) -> _TenantBundle:
    if tenant_id not in self._bundles:
        self._bundles[tenant_id] = _TenantBundle(tenant_id, self.config)
    return self._bundles[tenant_id]

This means the first LongTermMemory(...) call for a tenant creates a SQLite file at {persistence_path}/{tenant_id}_ltm.db (and runs CREATE TABLE IF NOT EXISTS) the moment that tenant is first touched — not at AIMemoryManager.__init__ time.

Concurrency model

There is no genuinely asynchronous I/O anywhere in this package — no asyncio-native database driver, no async HTTP calls. "Async support" throughout the codebase means one of two things:

  1. Thread-offloading, defined per-class rather than inherited — BaseRetriever (not BaseMemory) declares asave_context / aload_memory_variables, each calling asyncio.to_thread(self.save_context, ...). BaseMemory itself declares no async methods at all. Instead, every concrete memory class — ConversationBufferMemory, ConversationBufferWindowMemory, ConversationSummaryMemory, ConversationEntityMemory — independently re-implements an identical pair of asave_context/aload_memory_variables wrappers at the bottom of its own file. See Known Issues — a new BaseMemory subclass gets no async support unless it copies this boilerplate itself.
  2. ThreadPoolExecutor + run_in_executor — AIMemoryManager.asave_interaction, abuild_context, and arun_maintenance submit the corresponding sync method to self._executor (a ThreadPoolExecutor(max_workers=max_workers), default 4 workers) via loop.run_in_executor.
  3. Additionally, save_interaction itself (even the synchronous entry point) fires-and-forgets bundle.semantic.add and bundle.ltm.store onto self._executor — meaning semantic indexing and LTM promotion happen in the background relative to the caller, with no mechanism exposed to await or check completion.

Thread safety:

  • LongTermMemory uses a threading.Lock around every write operation and opens a fresh SQLite connection per call (check_same_thread=False), so concurrent access from multiple threads is safe.
  • UserProfileManager uses a threading.RLock around profile creation/lookup.
  • ShortTermMemory, WorkingMemory, SemanticMemory, MemorySelector, MemoryCompressor, and ForgettingMechanism hold no locks — they are not documented or guaranteed thread-safe for concurrent mutation. Because AIMemoryManager.save_interaction fires semantic/LTM writes onto a background thread pool while the main thread continues, concurrent calls to save_interaction for the same tenant can race on bundle.semantic._store / bundle.semantic._index (a plain dict and list with no lock).

[!WARNING] Race condition risk If your application calls save_interaction for the same tenant_id from multiple threads concurrently, the unsynchronized mutation of SemanticMemory's internal dict/index is a genuine race condition risk. Serialize writes per tenant, or wrap the manager's semantic/working-memory access with your own lock, if you need strict concurrent safety.

Multi-tenancy

Every stateful subsystem accepts a tenant_id and stores its own isolated copy of the data:

  • ShortTermMemory, WorkingMemory: tenant_id is stored in constructed entries' metadata but does not partition storage — one instance per tenant is created by _TenantBundle, so isolation is structural (separate objects), not filtered.
  • LongTermMemory: partitions rows by a tenant_id column in a single SQLite file — but AIMemoryManager still gives each tenant its own file ({tenant_id}_ltm.db), so the tenant_id column inside LongTermMemory is only meaningful if you construct and share one LongTermMemory instance across tenants yourself.
  • SemanticMemory: partitions via an ID prefix (f"{tenant_id}::{entry_id}") inside a single index — again, one instance per tenant in practice via _TenantBundle.

Extension points

The package does not define a formal plugin system, but the following seams are designed for substitution:

  • BaseMemory / BaseRetriever (core/base) — abstract base classes with save_context, load_memory_variables, clear (memory) or add_documents, get_relevant_documents (retriever). Any object implementing this interface can be used wherever the built-in memories are used.
  • LLM protocol — ConversationSummaryMemory, ConversationEntityMemory, MemoryCompressor, and AIMemoryManager all accept any object with a .generate(prompt: str, max_tokens: int) -> str method (informally a duck-typed Protocol, formally declared as LLMProtocol in memory_compressor.py and memory_manager.py). No base LLM class is required.
  • Embedding backend — SemanticMemory swaps between FAISS and pure-NumPy indexing automatically based on whether faiss is importable; you cannot select this explicitly, only influence it by installing/uninstalling FAISS.

Related pages