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():
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:
- Thread-offloading, defined per-class rather than inherited —
BaseRetriever(notBaseMemory) declaresasave_context/aload_memory_variables, each callingasyncio.to_thread(self.save_context, ...).BaseMemoryitself declares no async methods at all. Instead, every concrete memory class —ConversationBufferMemory,ConversationBufferWindowMemory,ConversationSummaryMemory,ConversationEntityMemory— independently re-implements an identical pair ofasave_context/aload_memory_variableswrappers at the bottom of its own file. See Known Issues — a newBaseMemorysubclass gets no async support unless it copies this boilerplate itself. ThreadPoolExecutor+run_in_executor—AIMemoryManager.asave_interaction,abuild_context, andarun_maintenancesubmit the corresponding sync method toself._executor(aThreadPoolExecutor(max_workers=max_workers), default 4 workers) vialoop.run_in_executor.- Additionally,
save_interactionitself (even the synchronous entry point) fires-and-forgetsbundle.semantic.addandbundle.ltm.storeontoself._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:
LongTermMemoryuses athreading.Lockaround every write operation and opens a fresh SQLite connection per call (check_same_thread=False), so concurrent access from multiple threads is safe.UserProfileManageruses athreading.RLockaround profile creation/lookup.ShortTermMemory,WorkingMemory,SemanticMemory,MemorySelector,MemoryCompressor, andForgettingMechanismhold no locks — they are not documented or guaranteed thread-safe for concurrent mutation. BecauseAIMemoryManager.save_interactionfires semantic/LTM writes onto a background thread pool while the main thread continues, concurrent calls tosave_interactionfor the same tenant can race onbundle.semantic._store/bundle.semantic._index(a plain dict and list with no lock).
[!WARNING] Race condition risk If your application calls
save_interactionfor the sametenant_idfrom multiple threads concurrently, the unsynchronized mutation ofSemanticMemory'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_idis 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 atenant_idcolumn in a single SQLite file — butAIMemoryManagerstill gives each tenant its own file ({tenant_id}_ltm.db), so thetenant_idcolumn insideLongTermMemoryis only meaningful if you construct and share oneLongTermMemoryinstance 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 withsave_context,load_memory_variables,clear(memory) oradd_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, andAIMemoryManagerall accept any object with a.generate(prompt: str, max_tokens: int) -> strmethod (informally a duck-typedProtocol, formally declared asLLMProtocolinmemory_compressor.pyandmemory_manager.py). No base LLM class is required. - Embedding backend —
SemanticMemoryswaps between FAISS and pure-NumPy indexing automatically based on whetherfaissis importable; you cannot select this explicitly, only influence it by installing/uninstalling FAISS.