Memory¶
Three orthogonal layers, each with its own contract:
- Conversation management — what to do with the message history within a single run (keep, window, summarize) when it grows past the model's context window.
- Cross-thread store — durable key-value scoped by namespace, used for long-term memory entries the agent reads at session start and writes at session end.
- Long-term memory manager — the policy layer that decides what
to extract from a run and when to retrieve, sitting on top of any
BaseStorebackend.
For checkpointing (state persistence between runs), see
Checkpointers — those backends live in
locus.memory.backends, with S3-compatible object storage as a
production target. Note those are checkpointer backends (per-thread
state); the cross-thread store layer below is a separate contract.
For the concepts, start with Long-term memory, Conversation management and the cross-thread store.
Conversation management¶
ConversationManager ¶
Bases: ABC
Base class for conversation management strategies.
Conversation managers handle how message history is maintained, including trimming, summarization, and other memory management.
apply
abstractmethod
¶
async_apply
async
¶
Async version of apply. Supports async summarization functions.
Default implementation delegates to the synchronous apply(). Override in subclasses that need async operations (e.g., LLM summarization).
Source code in src/locus/memory/conversation.py
NullManager ¶
Bases: ConversationManager
Null conversation manager - keeps all messages unchanged.
Use this when you want to preserve the entire conversation history without any modifications. Suitable for short conversations or when full history is required.
apply ¶
async_apply
async
¶
Async version of apply. Supports async summarization functions.
Default implementation delegates to the synchronous apply(). Override in subclasses that need async operations (e.g., LLM summarization).
Source code in src/locus/memory/conversation.py
SlidingWindowManager ¶
SlidingWindowManager(window_size: int = 20, preserve_system: bool = True, preserve_first_user: bool = True, slide_step: int = 1)
Bases: ConversationManager
Sliding window conversation manager - keeps last N messages.
Preserves the system message (if present) and the last N messages from the conversation. This is a simple and effective strategy for managing conversation length.
When the window would retain no user turn at all, the opening request is
re-attached. That case is an agent loop — assistant/tool all the way down —
where the first user message is the task, and losing it leaves the model
working from a role description and a wall of tool output with no statement
of what it was asked to do. It reads as drift rather than as an error, and
on Qwen-family chat templates it fails outright with No user query found
in messages.. Twenty tool round-trips reach the default window, so this is
the ordinary case for a tool-using agent, not an edge one.
A chat is left alone: while any user turn survives in the window, the newest ones are the live context and the first is not privileged.
Only the leading system messages (the instructions, a memory block right after them) are kept at the front. A system note the loop added mid-run stays where it was written and leaves with the turns around it: moving it to the front would change the start of every request after it, and a provider's prompt cache serves only a byte-identical prefix.
For the same reason the window can slide in steps. With slide_step=1
the oldest message goes on every request once the window is full, so the
history after the system prompt is different every time and nothing past
it is ever served from cache. A larger step drops that many messages at
once and then leaves the start alone until the window fills again: the
window holds between window_size - slide_step + 1 and window_size
messages, and all but one request in slide_step reuses the cache.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
window_size
|
int
|
Maximum number of messages to keep (excluding system message) |
20
|
preserve_system
|
bool
|
Whether to preserve the leading system messages at the start |
True
|
preserve_first_user
|
bool
|
Whether to re-attach the opening user turn when the window would otherwise contain none |
True
|
slide_step
|
int
|
How many messages the window drops at a time once full. |
1
|
Source code in src/locus/memory/conversation.py
apply ¶
Apply sliding window to messages.
Keeps the system message (if preserve_system is True) and the last window_size messages, re-attaching the opening user turn when the window would otherwise retain none (see the class docstring).
Source code in src/locus/memory/conversation.py
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | |
async_apply
async
¶
Async version of apply. Supports async summarization functions.
Default implementation delegates to the synchronous apply(). Override in subclasses that need async operations (e.g., LLM summarization).
Source code in src/locus/memory/conversation.py
SummarizingManager ¶
SummarizingManager(threshold: int = 30, keep_recent: int = 10, summarize_fn: Any | None = None, preserve_system: bool = True)
Bases: ConversationManager
Summarizing conversation manager - summarizes older messages.
When the conversation exceeds a threshold, older messages are summarized into a single summary message, preserving recent context.
This manager requires a summarization function to be provided, which can use an LLM to generate summaries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
int
|
Number of messages before summarization kicks in |
30
|
keep_recent
|
int
|
Number of recent messages to always preserve |
10
|
summarize_fn
|
Any | None
|
Async function that summarizes a list of messages |
None
|
preserve_system
|
bool
|
Whether to preserve the system message |
True
|
Source code in src/locus/memory/conversation.py
apply ¶
Apply summarization to messages.
If total messages exceed threshold, older messages are summarized. Note: If no summarize_fn is provided, falls back to a simple summary.
Source code in src/locus/memory/conversation.py
async_apply
async
¶
Async apply with LLM summarization support.
If summarize_fn is an async function, it will be properly awaited. Falls back to synchronous _generate_summary() otherwise.
Source code in src/locus/memory/conversation.py
Cross-thread store¶
The cross-thread KV the long-term memory manager writes to. Four implementations ship:
| Store | Backing | Reach for it when |
|---|---|---|
InMemoryStore |
in-process dict | tests, and anything that can lose memory on exit |
HolographicStore |
SQLite + FTS5 + holographic reduced representations | you want durable memory with no external infrastructure |
PgMemory |
Postgres + pgvector, per-tenant row-level security | multi-tenant, where isolation has to hold at the database |
OracleStore |
Oracle Database 26ai — native VECTOR + JSON columns |
memories next to the rest of your Oracle data, with semantic search in SQL |
The drivers in locus.memory.backends referenced by
Checkpointers implement BaseCheckpointer (per-thread
state), not BaseStore. The two layers are separate contracts and a
backend for one is not a backend for the other.
BaseStore ¶
Bases: ABC
Abstract base class for store implementations.
Provides common functionality and defines the interface.
capabilities
property
¶
Return capabilities. Override in subclasses.
put
abstractmethod
async
¶
get
abstractmethod
async
¶
delete
abstractmethod
async
¶
list_keys
abstractmethod
async
¶
exists
async
¶
get_item
async
¶
Get full item with metadata.
Source code in src/locus/memory/store.py
search
async
¶
Search for items in namespace.
Optional method — backends advertise support via
capabilities.search. Subclasses that support search must
override this method; the default raises
:class:StoreCapabilityError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to search in |
required |
query
|
str | None
|
Search query (implementation-specific) |
None
|
limit
|
int
|
Maximum results |
10
|
Returns:
| Type | Description |
|---|---|
list[StoreItem]
|
List of matching items |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support search. |
Source code in src/locus/memory/store.py
search_many
async
¶
search_many(namespaces: Sequence[tuple[str, ...]], query: str | None = None, limit: int = 10) -> list[list[StoreItem]]
:meth:search over several namespaces at once.
Returns one list per namespace, in the order given:
[await search(ns, query, limit) for ns in namespaces], which is
exactly what this default does. A backend that can answer them in one
round trip overrides it (PgMemory does, in one statement); callers
such as :class:~locus.memory.manager.LLMMemoryManager use it so a
recall over every memory type costs one store call instead of one per
type.
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support search. |
Source code in src/locus/memory/store.py
put_with_embedding
async
¶
put_with_embedding(namespace: tuple[str, ...], key: str, value: Any, embedding: list[float], metadata: dict[str, Any] | None = None) -> None
Store a value with its embedding vector for semantic search.
Optional method — backends advertise support via
capabilities.semantic_search. Subclasses that support
semantic search must override this method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Hierarchical namespace tuple |
required |
key
|
str
|
Key within the namespace |
required |
value
|
Any
|
Value to store (must be JSON-serializable) |
required |
embedding
|
list[float]
|
Vector embedding (e.g., from OpenAI, Cohere, etc.) |
required |
metadata
|
dict[str, Any] | None
|
Optional metadata for filtering |
None
|
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Example
Get embedding from your provider¶
embedding = await embedder.embed("User prefers dark theme")
await store.put_with_embedding( ("users", user_id, "memories"), "theme_preference", {"theme": "dark", "reason": "easier on eyes"}, embedding=embedding, metadata={"category": "preferences"} )
Source code in src/locus/memory/store.py
search_by_embedding
async
¶
search_by_embedding(namespace: tuple[str, ...], query_embedding: list[float], limit: int = 10, threshold: float | None = None, metadata_filter: dict[str, Any] | None = None) -> list[SemanticSearchResult]
Search for similar items using vector similarity.
Optional method — see capabilities.semantic_search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to search in |
required |
query_embedding
|
list[float]
|
Vector to compare against stored embeddings |
required |
limit
|
int
|
Maximum results to return |
10
|
threshold
|
float | None
|
Minimum similarity score (0.0-1.0), or None for no threshold |
None
|
metadata_filter
|
dict[str, Any] | None
|
Optional metadata constraints |
None
|
Returns:
| Type | Description |
|---|---|
list[SemanticSearchResult]
|
List of SemanticSearchResult, sorted by similarity (highest first) |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Source code in src/locus/memory/store.py
get_embedding
async
¶
Get the embedding vector for a stored item.
Optional method — see capabilities.semantic_search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Hierarchical namespace tuple |
required |
key
|
str
|
Key within the namespace |
required |
Returns:
| Type | Description |
|---|---|
list[float] | None
|
Embedding vector or None if not found / no embedding stored. |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Source code in src/locus/memory/store.py
put_batch
async
¶
Store multiple items.
Backends that advertise capabilities.batch_operations should
override this method to dispatch atomically. The default falls
back to sequential put() calls so callers always get
functional batching, just without atomicity guarantees.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
list[tuple[tuple[str, ...], str, Any, dict[str, Any] | None]]
|
List of (namespace, key, value, metadata) tuples |
required |
Source code in src/locus/memory/store.py
get_batch
async
¶
Retrieve multiple items.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[tuple[tuple[str, ...], str]]
|
List of (namespace, key) tuples |
required |
Returns:
| Type | Description |
|---|---|
dict[tuple[tuple[str, ...], str], Any]
|
Dict mapping (namespace, key) to value |
Source code in src/locus/memory/store.py
list_namespaces
async
¶
List all namespaces.
Optional method — see capabilities.list_namespaces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
tuple[str, ...] | None
|
Optional prefix to filter namespaces |
None
|
limit
|
int
|
Maximum namespaces to return |
100
|
Returns:
| Type | Description |
|---|---|
list[tuple[str, ...]]
|
List of namespace tuples |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support namespace listing. |
Source code in src/locus/memory/store.py
clear_namespace
async
¶
Delete all items in a namespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to clear |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of items deleted |
Source code in src/locus/memory/store.py
InMemoryStore ¶
Bases: BaseStore
In-memory store implementation.
Fast but not persistent - data is lost when process exits. Useful for testing and development.
Source code in src/locus/memory/store.py
exists
async
¶
search_many
async
¶
search_many(namespaces: Sequence[tuple[str, ...]], query: str | None = None, limit: int = 10) -> list[list[StoreItem]]
:meth:search over several namespaces at once.
Returns one list per namespace, in the order given:
[await search(ns, query, limit) for ns in namespaces], which is
exactly what this default does. A backend that can answer them in one
round trip overrides it (PgMemory does, in one statement); callers
such as :class:~locus.memory.manager.LLMMemoryManager use it so a
recall over every memory type costs one store call instead of one per
type.
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support search. |
Source code in src/locus/memory/store.py
put_with_embedding
async
¶
put_with_embedding(namespace: tuple[str, ...], key: str, value: Any, embedding: list[float], metadata: dict[str, Any] | None = None) -> None
Store a value with its embedding vector for semantic search.
Optional method — backends advertise support via
capabilities.semantic_search. Subclasses that support
semantic search must override this method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Hierarchical namespace tuple |
required |
key
|
str
|
Key within the namespace |
required |
value
|
Any
|
Value to store (must be JSON-serializable) |
required |
embedding
|
list[float]
|
Vector embedding (e.g., from OpenAI, Cohere, etc.) |
required |
metadata
|
dict[str, Any] | None
|
Optional metadata for filtering |
None
|
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Example
Get embedding from your provider¶
embedding = await embedder.embed("User prefers dark theme")
await store.put_with_embedding( ("users", user_id, "memories"), "theme_preference", {"theme": "dark", "reason": "easier on eyes"}, embedding=embedding, metadata={"category": "preferences"} )
Source code in src/locus/memory/store.py
search_by_embedding
async
¶
search_by_embedding(namespace: tuple[str, ...], query_embedding: list[float], limit: int = 10, threshold: float | None = None, metadata_filter: dict[str, Any] | None = None) -> list[SemanticSearchResult]
Search for similar items using vector similarity.
Optional method — see capabilities.semantic_search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to search in |
required |
query_embedding
|
list[float]
|
Vector to compare against stored embeddings |
required |
limit
|
int
|
Maximum results to return |
10
|
threshold
|
float | None
|
Minimum similarity score (0.0-1.0), or None for no threshold |
None
|
metadata_filter
|
dict[str, Any] | None
|
Optional metadata constraints |
None
|
Returns:
| Type | Description |
|---|---|
list[SemanticSearchResult]
|
List of SemanticSearchResult, sorted by similarity (highest first) |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Source code in src/locus/memory/store.py
get_embedding
async
¶
Get the embedding vector for a stored item.
Optional method — see capabilities.semantic_search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Hierarchical namespace tuple |
required |
key
|
str
|
Key within the namespace |
required |
Returns:
| Type | Description |
|---|---|
list[float] | None
|
Embedding vector or None if not found / no embedding stored. |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Source code in src/locus/memory/store.py
get_batch
async
¶
Retrieve multiple items.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[tuple[tuple[str, ...], str]]
|
List of (namespace, key) tuples |
required |
Returns:
| Type | Description |
|---|---|
dict[tuple[tuple[str, ...], str], Any]
|
Dict mapping (namespace, key) to value |
Source code in src/locus/memory/store.py
clear_namespace
async
¶
Delete all items in a namespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to clear |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of items deleted |
Source code in src/locus/memory/store.py
NamespacedStore ¶
Store wrapper with a fixed namespace prefix.
Makes it easier to work with a specific scope.
Example
user_store = NamespacedStore(store, ("users", user_id)) await user_store.put("preferences", {"theme": "dark"}) prefs = await user_store.get("preferences")
Source code in src/locus/memory/store.py
scoped ¶
StoreContext ¶
Context object passed to nodes for store access.
Provides convenient methods for common memory operations.
Example
async def my_node(inputs, *, store: StoreContext): # Get user memory user_prefs = await store.get_user_memory("preferences")
# Remember something
await store.remember(
"discussed_topic",
{"topic": "python", "timestamp": now}
)
# Search memories
related = await store.search_memories("python")
Source code in src/locus/memory/store.py
for_user ¶
Get namespaced store for a user.
If user_id is not provided, uses the user_id from the context.
Source code in src/locus/memory/store.py
for_session ¶
Get namespaced store for a session.
If session_id is not provided, uses the session_id from the context.
Source code in src/locus/memory/store.py
get_user_memory
async
¶
remember
async
¶
Store a memory for the current user.
Source code in src/locus/memory/store.py
forget
async
¶
search_memories
async
¶
Search user memories (full-text).
Source code in src/locus/memory/store.py
remember_with_embedding
async
¶
remember_with_embedding(key: str, value: Any, embedding: list[float], metadata: dict[str, Any] | None = None) -> None
Store a memory with its embedding for semantic search.
Requires: capabilities.semantic_search = True
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
Memory key |
required |
value
|
Any
|
Value to store |
required |
embedding
|
list[float]
|
Vector embedding for semantic search |
required |
metadata
|
dict[str, Any] | None
|
Optional metadata |
None
|
Example
embedding = await embedder.embed("User likes dark theme") await store.remember_with_embedding( "theme_pref", {"theme": "dark"}, embedding=embedding, )
Source code in src/locus/memory/store.py
search_memories_semantic
async
¶
search_memories_semantic(query_embedding: list[float], limit: int = 10, threshold: float | None = None) -> list[SemanticSearchResult]
Search user memories by semantic similarity.
Requires: capabilities.semantic_search = True
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query_embedding
|
list[float]
|
Vector to search with |
required |
limit
|
int
|
Maximum results |
10
|
threshold
|
float | None
|
Minimum similarity score (0.0-1.0) |
None
|
Returns:
| Type | Description |
|---|---|
list[SemanticSearchResult]
|
List of SemanticSearchResult sorted by similarity |
Example
query_vec = await embedder.embed("user preferences") results = await store.search_memories_semantic( query_embedding=query_vec, limit=5, threshold=0.7, ) for r in results: print(f"{r.item.key}: {r.score:.2f} similar")
Source code in src/locus/memory/store.py
get_global
async
¶
set_global
async
¶
StoreItem
dataclass
¶
StoreItem(namespace: tuple[str, ...], key: str, value: Any, metadata: dict[str, Any], created_at: datetime, updated_at: datetime, version: int = 1)
An item stored in the store.
Attributes:
| Name | Type | Description |
|---|---|---|
namespace |
tuple[str, ...]
|
The namespace tuple |
key |
str
|
The key within namespace |
value |
Any
|
The stored value |
metadata |
dict[str, Any]
|
Optional metadata |
created_at |
datetime
|
Creation timestamp |
updated_at |
datetime
|
Last update timestamp |
version |
int
|
Version counter for optimistic locking |
to_dict ¶
Convert to dictionary.
Source code in src/locus/memory/store.py
from_dict
classmethod
¶
Create from dictionary.
Source code in src/locus/memory/store.py
StoreCapabilities
dataclass
¶
StoreCapabilities(search: bool = False, semantic_search: bool = False, embedding_dimension: int | None = None, ttl: bool = False, list_namespaces: bool = False, batch_operations: bool = False, transactions: bool = False)
Capabilities supported by a store implementation.
Use this to discover what features a store supports.
StoreCapabilityError ¶
Bases: NotImplementedError
Raised when a store operation is requested that the backend does not support.
Carries structured context (capability, store_class, hint)
so callers can branch on the capability or surface a clean message.
Subclasses :class:NotImplementedError so legacy except
NotImplementedError blocks continue to catch it — this is strictly
a richer payload, not a behaviour change.
Example::
try:
results = await store.search(("users", uid), query="...")
except StoreCapabilityError as exc:
log.info(
"store %s does not support %s; falling back",
exc.store_class,
exc.capability,
)
results = await store.list_keys(("users", uid))
Source code in src/locus/memory/store.py
SemanticSearchResult
dataclass
¶
Result from semantic (vector similarity) search.
Attributes:
| Name | Type | Description |
|---|---|---|
item |
StoreItem
|
The matching store item |
score |
float
|
Similarity score (0.0 to 1.0, higher is more similar) |
distance |
float | None
|
Raw distance metric (interpretation depends on distance type) |
Durable store backends¶
Both shipped in 2.2.0.
HolographicStore ¶
Bases: BaseStore
Local SQLite + FTS5 + HRR store — zero external infra.
Every value is kept as a row plus an FTS5 index entry and (when numpy is
present) an HRR phase vector. search ranks by HRR associative similarity,
fused with FTS5 lexical match; without numpy it is FTS5-only. A single
connection guarded by a lock keeps it safe across the store's threads.
Source code in src/locus/memory/store_backends/holographic.py
exists
async
¶
get_item
async
¶
Get full item with metadata.
Source code in src/locus/memory/store.py
search_many
async
¶
search_many(namespaces: Sequence[tuple[str, ...]], query: str | None = None, limit: int = 10) -> list[list[StoreItem]]
:meth:search over several namespaces at once.
Returns one list per namespace, in the order given:
[await search(ns, query, limit) for ns in namespaces], which is
exactly what this default does. A backend that can answer them in one
round trip overrides it (PgMemory does, in one statement); callers
such as :class:~locus.memory.manager.LLMMemoryManager use it so a
recall over every memory type costs one store call instead of one per
type.
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support search. |
Source code in src/locus/memory/store.py
put_with_embedding
async
¶
put_with_embedding(namespace: tuple[str, ...], key: str, value: Any, embedding: list[float], metadata: dict[str, Any] | None = None) -> None
Store a value with its embedding vector for semantic search.
Optional method — backends advertise support via
capabilities.semantic_search. Subclasses that support
semantic search must override this method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Hierarchical namespace tuple |
required |
key
|
str
|
Key within the namespace |
required |
value
|
Any
|
Value to store (must be JSON-serializable) |
required |
embedding
|
list[float]
|
Vector embedding (e.g., from OpenAI, Cohere, etc.) |
required |
metadata
|
dict[str, Any] | None
|
Optional metadata for filtering |
None
|
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Example
Get embedding from your provider¶
embedding = await embedder.embed("User prefers dark theme")
await store.put_with_embedding( ("users", user_id, "memories"), "theme_preference", {"theme": "dark", "reason": "easier on eyes"}, embedding=embedding, metadata={"category": "preferences"} )
Source code in src/locus/memory/store.py
search_by_embedding
async
¶
search_by_embedding(namespace: tuple[str, ...], query_embedding: list[float], limit: int = 10, threshold: float | None = None, metadata_filter: dict[str, Any] | None = None) -> list[SemanticSearchResult]
Search for similar items using vector similarity.
Optional method — see capabilities.semantic_search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to search in |
required |
query_embedding
|
list[float]
|
Vector to compare against stored embeddings |
required |
limit
|
int
|
Maximum results to return |
10
|
threshold
|
float | None
|
Minimum similarity score (0.0-1.0), or None for no threshold |
None
|
metadata_filter
|
dict[str, Any] | None
|
Optional metadata constraints |
None
|
Returns:
| Type | Description |
|---|---|
list[SemanticSearchResult]
|
List of SemanticSearchResult, sorted by similarity (highest first) |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Source code in src/locus/memory/store.py
get_embedding
async
¶
Get the embedding vector for a stored item.
Optional method — see capabilities.semantic_search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Hierarchical namespace tuple |
required |
key
|
str
|
Key within the namespace |
required |
Returns:
| Type | Description |
|---|---|
list[float] | None
|
Embedding vector or None if not found / no embedding stored. |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Source code in src/locus/memory/store.py
put_batch
async
¶
Store multiple items.
Backends that advertise capabilities.batch_operations should
override this method to dispatch atomically. The default falls
back to sequential put() calls so callers always get
functional batching, just without atomicity guarantees.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
list[tuple[tuple[str, ...], str, Any, dict[str, Any] | None]]
|
List of (namespace, key, value, metadata) tuples |
required |
Source code in src/locus/memory/store.py
get_batch
async
¶
Retrieve multiple items.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[tuple[tuple[str, ...], str]]
|
List of (namespace, key) tuples |
required |
Returns:
| Type | Description |
|---|---|
dict[tuple[tuple[str, ...], str], Any]
|
Dict mapping (namespace, key) to value |
Source code in src/locus/memory/store.py
list_namespaces
async
¶
List all namespaces.
Optional method — see capabilities.list_namespaces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
tuple[str, ...] | None
|
Optional prefix to filter namespaces |
None
|
limit
|
int
|
Maximum namespaces to return |
100
|
Returns:
| Type | Description |
|---|---|
list[tuple[str, ...]]
|
List of namespace tuples |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support namespace listing. |
Source code in src/locus/memory/store.py
clear_namespace
async
¶
Delete all items in a namespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to clear |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of items deleted |
Source code in src/locus/memory/store.py
PgMemory ¶
PgMemory(dsn: str, *, table: str = 'locus_memories', dim: int = _DEFAULT_PG_DIM, embedder: BaseEmbedding | None = None, encoding_cache: int = _DEFAULT_ENCODING_CACHE)
Bases: BaseStore
Postgres + pgvector memory store with per-tenant Row-Level Security.
namespace[0] is the tenant (the isolation boundary); the full namespace
tuple scopes within a tenant (e.g. (tenant, "user", user_id)). The schema,
RLS policy, and HNSW index are created on first use, so it works against a
bare Postgres locally and a managed Aurora cluster identically.
Recall quality depends on the vector. Pass an embedder (any
:class:~locus.rag.embeddings.base.BaseEmbedding, e.g. brokered OpenAI
text-embedding-3-small) for true semantic recall — the enterprise
default. Without one it falls back to the HRR [cos φ, sin φ] encoding,
which is lexical/associative (matches shared or hashed tokens), not
trained semantics — fine offline, but it will not match paraphrases the way
an embedding model does. Choose the embedder to match the value of recall.
Width and the ANN index. Without an embedder the pgvector column is
2·dim wide (the [cos φ, sin φ] pair), and pgvector will not build an
HNSW index over more than :data:PGVECTOR_ANN_MAX_DIM dimensions. dim
therefore defaults to 512 (a 1024-wide column) and a larger explicit dim
is rejected at construction rather than at CREATE INDEX. An embedder
wider than the limit is allowed — its width is the model's, not ours — but
the table is then created without an ANN index and the constructor emits a
RuntimeWarning saying so.
numpy. The HRR encoding needs numpy (the [pgvector] extra installs
it). Without numpy and without an embedder nothing is wrong at write time,
but rows are stored with no vector and recall degrades to a substring match;
the constructor warns (RuntimeWarning) and the first degraded search
logs a warning, so this never happens quietly.
Encoding cache. Encoding a text costs a pure-Python HRR pass (or an
embedding call), and one recall often encodes the same words several times.
Each store keeps the last encoding_cache encodings (0 turns the
cache off), keyed by (tenant, text): an entry is only ever reused within
the tenant that computed it, so the cache is not a surface two tenants
share.
Several namespaces at once. :meth:search_many ranks several namespaces
of ONE tenant in one transaction and one statement, encoding the query once.
Namespaces of different tenants in one call are refused.
Source code in src/locus/memory/store_backends/postgresql.py
search_many
async
¶
search_many(namespaces: Sequence[tuple[str, ...]], query: str | None = None, limit: int = 10) -> list[list[StoreItem]]
:meth:search over several namespaces of ONE tenant, in one statement.
One transaction (the tenant pin and one SELECT), the query encoded
once, and each namespace ranked on its own exactly as :meth:search
would rank it (LATERAL per namespace), so the result is
[search(ns, query, limit) for ns in namespaces] at the cost of one
round trip.
Raises:
| Type | Description |
|---|---|
ValueError
|
The namespaces belong to more than one tenant
( |
Source code in src/locus/memory/store_backends/postgresql.py
exists
async
¶
get_item
async
¶
Get full item with metadata.
Source code in src/locus/memory/store.py
put_with_embedding
async
¶
put_with_embedding(namespace: tuple[str, ...], key: str, value: Any, embedding: list[float], metadata: dict[str, Any] | None = None) -> None
Store a value with its embedding vector for semantic search.
Optional method — backends advertise support via
capabilities.semantic_search. Subclasses that support
semantic search must override this method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Hierarchical namespace tuple |
required |
key
|
str
|
Key within the namespace |
required |
value
|
Any
|
Value to store (must be JSON-serializable) |
required |
embedding
|
list[float]
|
Vector embedding (e.g., from OpenAI, Cohere, etc.) |
required |
metadata
|
dict[str, Any] | None
|
Optional metadata for filtering |
None
|
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Example
Get embedding from your provider¶
embedding = await embedder.embed("User prefers dark theme")
await store.put_with_embedding( ("users", user_id, "memories"), "theme_preference", {"theme": "dark", "reason": "easier on eyes"}, embedding=embedding, metadata={"category": "preferences"} )
Source code in src/locus/memory/store.py
search_by_embedding
async
¶
search_by_embedding(namespace: tuple[str, ...], query_embedding: list[float], limit: int = 10, threshold: float | None = None, metadata_filter: dict[str, Any] | None = None) -> list[SemanticSearchResult]
Search for similar items using vector similarity.
Optional method — see capabilities.semantic_search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to search in |
required |
query_embedding
|
list[float]
|
Vector to compare against stored embeddings |
required |
limit
|
int
|
Maximum results to return |
10
|
threshold
|
float | None
|
Minimum similarity score (0.0-1.0), or None for no threshold |
None
|
metadata_filter
|
dict[str, Any] | None
|
Optional metadata constraints |
None
|
Returns:
| Type | Description |
|---|---|
list[SemanticSearchResult]
|
List of SemanticSearchResult, sorted by similarity (highest first) |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Source code in src/locus/memory/store.py
get_embedding
async
¶
Get the embedding vector for a stored item.
Optional method — see capabilities.semantic_search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Hierarchical namespace tuple |
required |
key
|
str
|
Key within the namespace |
required |
Returns:
| Type | Description |
|---|---|
list[float] | None
|
Embedding vector or None if not found / no embedding stored. |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Source code in src/locus/memory/store.py
put_batch
async
¶
Store multiple items.
Backends that advertise capabilities.batch_operations should
override this method to dispatch atomically. The default falls
back to sequential put() calls so callers always get
functional batching, just without atomicity guarantees.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
list[tuple[tuple[str, ...], str, Any, dict[str, Any] | None]]
|
List of (namespace, key, value, metadata) tuples |
required |
Source code in src/locus/memory/store.py
get_batch
async
¶
Retrieve multiple items.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[tuple[tuple[str, ...], str]]
|
List of (namespace, key) tuples |
required |
Returns:
| Type | Description |
|---|---|
dict[tuple[tuple[str, ...], str], Any]
|
Dict mapping (namespace, key) to value |
Source code in src/locus/memory/store.py
list_namespaces
async
¶
List all namespaces.
Optional method — see capabilities.list_namespaces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
tuple[str, ...] | None
|
Optional prefix to filter namespaces |
None
|
limit
|
int
|
Maximum namespaces to return |
100
|
Returns:
| Type | Description |
|---|---|
list[tuple[str, ...]]
|
List of namespace tuples |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support namespace listing. |
Source code in src/locus/memory/store.py
clear_namespace
async
¶
Delete all items in a namespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to clear |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of items deleted |
Source code in src/locus/memory/store.py
OracleStore ¶
OracleStore(*, dsn: str | None = None, user: str = 'admin', password: str | SecretStr = '', wallet_location: str | None = None, wallet_password: str | SecretStr | None = None, host: str | None = None, port: int = 1521, service_name: str | None = None, table_name: str = 'locus_store', schema_name: str | None = None, min_pool_size: int = 1, max_pool_size: int = 5, dimension: int | None = None, distance_metric: str = 'COSINE', auto_create_table: bool = True)
Bases: BaseStore
Oracle Database 26ai-backed long-term memory store.
Implements the :class:locus.memory.store.BaseStore contract on top
of Oracle 26ai. Persists namespaced key/value rows and, when
dimension is set, vector embeddings for semantic search inside a
namespace.
Production setup — use a least-privileged schema, not ADMIN.
Running against ADMIN is an Oracle anti-pattern: every connection
has full DBA privileges, so a compromised credential has unbounded
blast radius. Provision a dedicated app user with only the
privileges this store needs::
CREATE USER locus_app IDENTIFIED BY "<strong-password>";
GRANT CONNECT, RESOURCE TO locus_app;
ALTER USER locus_app QUOTA 1G ON DATA;
Then either set auto_create_table=True and let the store
provision its own table, or pre-create it as DBA and pass
auto_create_table=False so the app user runs with DML-only.
Example — Autonomous DB with wallet, vector mode::
store = OracleStore(
dsn="mydb_high",
user="locus_app",
password=os.environ["LOCUS_DB_PASSWORD"],
wallet_location="~/.oci/wallets/mydb",
dimension=1024, # Cohere embed-v3
)
await store.put(("memory", "u1"), "theme", {"value": "dark"})
await store.put_with_embedding(
("memory", "u1"),
"fact1",
{"text": "user likes dark theme"},
embedding=embedder("user likes dark theme"),
)
hits = await store.search_by_embedding(
("memory", "u1"),
query_embedding=embedder("display preferences"),
limit=5,
)
Example — text-only mode (no vector column)::
store = OracleStore(
dsn="mydb_high",
user="u",
password="p",
dimension=None, # store still works; semantic_search disabled
)
Source code in src/locus/memory/store_backends/oracle.py
capabilities
property
¶
Report what this backend supports.
semantic_search is gated on dimension being set — the
VECTOR column is omitted from the DDL in text-only mode, so the
vector queries would fail at the SQL level anyway.
put
async
¶
put(namespace: tuple[str, ...], key: str, value: Any, metadata: dict[str, Any] | None = None) -> None
Upsert a value.
metadata is accepted for BaseStore parity but folded into
the JSON value when provided so we don't need a second column.
Callers needing first-class metadata should query
value['_meta'] from JSON_VALUE.
Source code in src/locus/memory/store_backends/oracle.py
search
async
¶
Text search inside a namespace.
Uses LOWER(value) LIKE LOWER(:q) — sufficient for the
small payloads memory stores typically carry. For
Oracle-Text-grade ranking, swap to CONTAINS() with an
Oracle Text index out-of-band.
Source code in src/locus/memory/store_backends/oracle.py
close
async
¶
exists
async
¶
get_item
async
¶
Get full item with metadata.
Source code in src/locus/memory/store.py
search_many
async
¶
search_many(namespaces: Sequence[tuple[str, ...]], query: str | None = None, limit: int = 10) -> list[list[StoreItem]]
:meth:search over several namespaces at once.
Returns one list per namespace, in the order given:
[await search(ns, query, limit) for ns in namespaces], which is
exactly what this default does. A backend that can answer them in one
round trip overrides it (PgMemory does, in one statement); callers
such as :class:~locus.memory.manager.LLMMemoryManager use it so a
recall over every memory type costs one store call instead of one per
type.
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support search. |
Source code in src/locus/memory/store.py
get_embedding
async
¶
Get the embedding vector for a stored item.
Optional method — see capabilities.semantic_search.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Hierarchical namespace tuple |
required |
key
|
str
|
Key within the namespace |
required |
Returns:
| Type | Description |
|---|---|
list[float] | None
|
Embedding vector or None if not found / no embedding stored. |
Raises:
| Type | Description |
|---|---|
StoreCapabilityError
|
If the backend does not support semantic search. |
Source code in src/locus/memory/store.py
put_batch
async
¶
Store multiple items.
Backends that advertise capabilities.batch_operations should
override this method to dispatch atomically. The default falls
back to sequential put() calls so callers always get
functional batching, just without atomicity guarantees.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
list[tuple[tuple[str, ...], str, Any, dict[str, Any] | None]]
|
List of (namespace, key, value, metadata) tuples |
required |
Source code in src/locus/memory/store.py
get_batch
async
¶
Retrieve multiple items.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[tuple[tuple[str, ...], str]]
|
List of (namespace, key) tuples |
required |
Returns:
| Type | Description |
|---|---|
dict[tuple[tuple[str, ...], str], Any]
|
Dict mapping (namespace, key) to value |
Source code in src/locus/memory/store.py
clear_namespace
async
¶
Delete all items in a namespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
tuple[str, ...]
|
Namespace to clear |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of items deleted |
Source code in src/locus/memory/store.py
OracleStoreConfig ¶
Bases: BaseModel
Configuration for :class:OracleStore.
Mirrors :class:locus.memory.backends.oracle.OracleConfig for the
connection envelope so the same set of arguments work for both
checkpointer and store side-by-side.
Long-term memory manager¶
LLMMemoryManager is the default — it uses an auxiliary model to
extract and categorize memories at session end and retrieves the
top-k relevant entries at session start. NoopMemoryManager is the
pass-through used in tests. Mem0MemoryManager (behind the mem0
extra, pip install "locus-sdk[mem0]") hands the finished
conversation to the open-source mem0 library, which does its own
extraction and keeps the memories in mem0's own vector store rather
than a BaseStore. OracleAgentMemoryManager (behind the agentmemory
extra) delegates to Oracle's official
oracleagentmemory client —
context-card summaries and scoped retrieval by user, agent and thread on
Oracle Database 26ai.
BaseMemoryManager ¶
Bases: ABC
Abstract base for long-term memory managers.
Subclasses implement :meth:extract to decide what is worth
remembering; the base class handles retrieval, injection, and the
session-start / session-end lifecycle hooks that the agent calls
automatically.
Two concrete implementations are provided:
- :class:
NoopMemoryManager— no-op, useful for testing. - :class:
LLMMemoryManager— persists to any :class:BaseStorebackend; accepts an optional LLM-backed extraction function.
pending_extractions
property
¶
Background extractions scheduled or running, not yet finished.
extract
abstractmethod
async
¶
Extract durable memories from a finished conversation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
messages
|
list[Message]
|
The full message history for the completed session. |
required |
Returns:
| Type | Description |
|---|---|
list[Memory]
|
List of :class: |
Source code in src/locus/memory/manager.py
retrieve
abstractmethod
async
¶
Retrieve stored memories for injection at session start.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int
|
Maximum number of memories to return. |
20
|
Returns:
| Type | Description |
|---|---|
list[Memory]
|
List of :class: |
Source code in src/locus/memory/manager.py
save
abstractmethod
async
¶
Persist a list of memories to the backing store.
Implementations should upsert by key — re-extracting the same fact updates the record rather than creating a duplicate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
memories
|
list[Memory]
|
Memories to persist. |
required |
Source code in src/locus/memory/manager.py
retrieve_relevant
async
¶
Retrieve the memories most relevant to query.
Called by :meth:on_session_start with the text of the current user
turn. The default ignores query and delegates to :meth:retrieve,
so subclasses written against the older retrieve(limit) contract
keep working unchanged; managers whose backend can rank by relevance
override this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str | None
|
Text to rank memories against, usually the latest user
message. |
required |
limit
|
int | None
|
Maximum number of memories to return. |
None
|
Source code in src/locus/memory/manager.py
on_session_start
async
¶
Retrieve memories and inject them into the agent state.
Called by the agent runtime at the start of every invocation,
after on_before_invocation hooks but before the first model
call. The latest user message is used as the retrieval query, and
the formatted block replaces any block injected by an earlier turn
of the same (checkpointed) thread instead of being added next to it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
AgentState
|
Current agent state (just-created or loaded from checkpointer). |
required |
Returns:
| Type | Description |
|---|---|
AgentState
|
Possibly-modified state with memory context injected. |
Source code in src/locus/memory/manager.py
on_session_end
async
¶
Extract memories from the finished session and save them.
Called by the agent runtime in the finally block of every
invocation, after on_after_invocation hooks but before the
final checkpoint.
With extract_mode="inline" (the default) extraction finishes before
this returns, so its latency is part of the run. With
extract_mode="background" it is scheduled as a tracked task and
this returns at once: the run ends — and a streaming consumer sees its
last event — without waiting for the extractor. Background jobs of one
namespace run in submission order (two turns of one user never race
their writes), at most :attr:max_concurrent_extractions run at once,
and :meth:drain waits for all of them (call it, or
Agent.drain_memory(), at graceful shutdown and in tests).
The injected memory block is removed before extraction, so recalled
memories are not fed back to the extractor as if the user had just
said them. A failing extractor or store is logged and reported as a
memory.manager.extract_failed event rather than raised, in either
mode: an auxiliary memory write must not cost the conversation its
turn (inline), and must never surface in a run that already finished
(background).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
AgentState
|
Final agent state with the complete message history. |
required |
Source code in src/locus/memory/manager.py
drain
async
¶
Wait for every scheduled background extraction to finish.
Jobs submitted while draining are waited for too. Safe to call in
"inline" mode (returns at once). Errors inside jobs were already
logged and emitted; they are not raised here. Bound the wait with
asyncio.timeout(...): cancelling drain leaves the jobs running.
Source code in src/locus/memory/manager.py
LLMMemoryManager ¶
LLMMemoryManager(store: BaseStore, *, extract_fn: ExtractFn | None = None, namespace_prefix: tuple[str, ...] = ('locus_memory',), max_memories: int = 50, retrieve_limit: int = 20, extract_mode: ExtractMode = 'inline', max_concurrent_extractions: int = 4, namespace_resolver: NamespaceResolver | None = None)
Bases: BaseMemoryManager
Memory manager backed by any :class:~locus.memory.store.BaseStore.
Extraction uses either a caller-supplied async function
(extract_fn) or a built-in heuristic that pattern-matches
common conversational signals (corrections, confirmations, role
disclosures). Supply extract_fn for production use; the
heuristic is adequate for demos and tests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
BaseStore
|
Any :class: |
required |
extract_fn
|
ExtractFn | None
|
Optional async callable
|
None
|
namespace_prefix
|
tuple[str, ...]
|
Store namespace prefix. Scope per user or
tenant by passing |
('locus_memory',)
|
max_memories
|
int
|
Hard cap on the total number of memories kept per type. Oldest entries are pruned when the limit is reached. |
50
|
retrieve_limit
|
int
|
Maximum memories returned by :meth: |
20
|
extract_mode
|
ExtractMode
|
|
'inline'
|
max_concurrent_extractions
|
int
|
Background extractions allowed to run at once. Jobs of one namespace always run one at a time, in order. |
4
|
namespace_resolver
|
NamespaceResolver | None
|
|
None
|
Example::
from locus.memory.manager import LLMMemoryManager
from locus.memory.store import InMemoryStore
manager = LLMMemoryManager(
store=InMemoryStore(),
extract_fn=my_llm_extractor,
namespace_prefix=("users", user_id),
)
agent = Agent(model="anthropic:claude-sonnet-4-6", memory_manager=manager)
Source code in src/locus/memory/manager.py
active_namespace
property
¶
The prefix the current call reads and writes under: the one a
:meth:scoped block (or the run's resolver) set, else
namespace_prefix.
active_recall
property
¶
Every prefix a recall in the current scope reads: the active
namespace first, then the scope's read-only recall prefixes.
pending_extractions
property
¶
Background extractions scheduled or running, not yet finished.
scoped ¶
scoped(namespace_prefix: tuple[str, ...], *, recall: Sequence[tuple[str, ...]] = ()) -> Iterator[None]
Run :meth:retrieve / :meth:save / :meth:extract calls — and
background jobs scheduled — inside the block under
namespace_prefix.
recall adds read-only prefixes that :meth:retrieve also reads;
writes still go to namespace_prefix alone. A recall prefix whose
first element differs from namespace_prefix[0] (another tenant) is
dropped with a warning.
Context-local: concurrent tasks each keep their own scope. The agent
runtime does this for you when namespace_resolver is set.
Source code in src/locus/memory/manager.py
on_session_start
async
¶
Inject this run's memories, read from the run's namespace (and the
read-only namespaces its :class:MemoryScope names) in one recall.
Source code in src/locus/memory/manager.py
on_session_end
async
¶
Extract and save this run's memories into the run's namespace.
A background job is created inside the scope, so it inherits it; it
shares this manager's one semaphore and one :meth:drain with every
other namespace's jobs.
Source code in src/locus/memory/manager.py
extract
async
¶
Extract memories from a message list.
Uses extract_fn when provided; otherwise applies the
built-in heuristic.
Source code in src/locus/memory/manager.py
retrieve
async
¶
retrieve(limit: int | None = None, *, query: str | None = None, namespaces: Sequence[tuple[str, ...]] | None = None) -> list[Memory]
Retrieve stored memories across every type.
Without query this returns the most recently updated memories.
With query it first asks the store to rank each type's memories
against it (BaseStore.search(namespace, query=...) — semantic on
PgMemory / HolographicStore), interleaves the best matches of
every type, then tops the list up with the most recent memories. The
top-up is what keeps a store whose search only filters (the
substring match of InMemoryStore) from injecting nothing just
because no memory contains the whole user message. Stores that cannot
search at all fall back to recency.
Every type of every namespace is read with one
:meth:~locus.memory.store.BaseStore.search_many call for the ranking
and one for the recency top-up — one round trip each on PgMemory
— instead of a search per type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
Maximum memories returned. |
None
|
query
|
str | None
|
Optional text to rank memories against. |
None
|
namespaces
|
Sequence[tuple[str, ...]] | None
|
Prefixes to recall from, in priority order. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in src/locus/memory/manager.py
retrieve_relevant
async
¶
retrieve_relevant(query: str | None, limit: int | None = None, *, namespaces: Sequence[tuple[str, ...]] | None = None) -> list[Memory]
Rank stored memories against query — see :meth:retrieve.
Source code in src/locus/memory/manager.py
save
async
¶
Upsert memories into the backing store.
Memories with the same key and type overwrite the
previous entry — no duplicates accumulate. Afterwards each touched
type is pruned to max_memories entries, oldest first.
Source code in src/locus/memory/manager.py
drain
async
¶
Wait for every scheduled background extraction to finish.
Jobs submitted while draining are waited for too. Safe to call in
"inline" mode (returns at once). Errors inside jobs were already
logged and emitted; they are not raised here. Bound the wait with
asyncio.timeout(...): cancelling drain leaves the jobs running.
Source code in src/locus/memory/manager.py
NoopMemoryManager ¶
Bases: BaseMemoryManager
Pass-through memory manager — stores and retrieves nothing.
Useful as a test double or as a placeholder when you want the
agent-wiring (memory_manager= kwarg) without actual persistence.
pending_extractions
property
¶
Background extractions scheduled or running, not yet finished.
retrieve_relevant
async
¶
Retrieve the memories most relevant to query.
Called by :meth:on_session_start with the text of the current user
turn. The default ignores query and delegates to :meth:retrieve,
so subclasses written against the older retrieve(limit) contract
keep working unchanged; managers whose backend can rank by relevance
override this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str | None
|
Text to rank memories against, usually the latest user
message. |
required |
limit
|
int | None
|
Maximum number of memories to return. |
None
|
Source code in src/locus/memory/manager.py
drain
async
¶
Wait for every scheduled background extraction to finish.
Jobs submitted while draining are waited for too. Safe to call in
"inline" mode (returns at once). Errors inside jobs were already
logged and emitted; they are not raised here. Bound the wait with
asyncio.timeout(...): cancelling drain leaves the jobs running.
Source code in src/locus/memory/manager.py
Memory
dataclass
¶
A single durable memory entry.
Attributes:
| Name | Type | Description |
|---|---|---|
type |
MemoryType
|
Semantic category (user / feedback / project / reference). |
key |
str
|
Stable logical name within the category. The store uses this as the key, so re-extracting the same fact under the same key updates the record rather than creating a duplicate. |
content |
str
|
The actual fact or rule, as a human-readable string. |
metadata |
dict[str, Any]
|
Arbitrary extra fields — |
MemoryType ¶
Bases: StrEnum
Semantic category for a stored memory.
OracleAgentMemoryManager ¶
OracleAgentMemoryManager(*, connection: Any = None, store: Any = None, embedder: Any = 'oci/cohere.embed-english-v3.0', llm: Any = 'oci/openai.gpt-4.1', extract_memories: bool = True, table_name_prefix: str = 'locus_oam_', schema_policy: Any = None, thread_id_resolver: ScopeResolver = _default_thread_id, user_id_resolver: Callable[[AgentState], str | None] = _default_user_id, agent_id_resolver: Callable[[AgentState], str | None] = _default_agent_id, max_context_messages: int = 50, max_relevant_results: int = 5, max_recent_messages: int = 5, thread_defaults: dict[str, Any] | None = None)
Bases: BaseMemoryManager
BaseMemoryManager backed by Oracle's oracleagentmemory client.
Compose with any Locus Agent via the standard
memory_manager= slot::
from locus.agent import Agent, AgentConfig
from locus.memory.managers import OracleAgentMemoryManager
manager = OracleAgentMemoryManager(
connection=oracledb.connect(
user="locus_app",
password=...,
dsn="mydb_low",
config_dir="~/.oci/wallets/mydb",
wallet_location="~/.oci/wallets/mydb",
wallet_password=...,
),
embedder="oci/cohere.embed-english-v3.0",
llm="oci/openai.gpt-4.1",
table_name_prefix="locus_oam_",
)
agent = Agent(
config=AgentConfig(
model=...,
memory_manager=manager,
)
)
# Pass user_id (and optional thread_id) via the `metadata` kwarg —
# the adapter reads them from state.metadata to scope reads + writes.
agent.run_sync(
"Hi, I'm Alice — I prefer us-chicago-1.",
metadata={"user_id": "alice", "thread_id": "t-a"},
)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Any
|
A pre-built |
None
|
store
|
Any
|
An already-constructed
|
None
|
embedder
|
Any
|
Either a litellm-style model spec ( |
'oci/cohere.embed-english-v3.0'
|
llm
|
Any
|
Either a litellm-style model spec or a custom |
'oci/openai.gpt-4.1'
|
extract_memories
|
bool
|
Whether the underlying client should run
its LLM-backed extractor when |
True
|
table_name_prefix
|
str
|
Prefix for the tables the client provisions
( |
'locus_oam_'
|
thread_id_resolver
|
ScopeResolver
|
Callable that returns the scoping
|
_default_thread_id
|
user_id_resolver
|
Callable[[AgentState], str | None]
|
Callable that returns the scoping
|
_default_user_id
|
agent_id_resolver
|
Callable[[AgentState], str | None]
|
Callable that returns the scoping
|
_default_agent_id
|
max_context_messages
|
int
|
Cap on how many recent messages we ship
to |
50
|
max_relevant_results
|
int
|
Forwarded to |
5
|
max_recent_messages
|
int
|
Forwarded to |
5
|
Source code in src/locus/memory/managers/oracle_agent_memory.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | |
pending_extractions
property
¶
Background extractions scheduled or running, not yet finished.
extract
async
¶
No-op for direct callers — the underlying client extracts
inside :meth:on_session_end via add_messages_async. We
return an empty list so :meth:save does nothing, avoiding a
double-write.
Source code in src/locus/memory/managers/oracle_agent_memory.py
retrieve
async
¶
Best-effort top-level retrieval — unscoped.
Direct callers without an :class:AgentState get the most
recent limit memories across every scope. For per-user
scoped retrieval, use :meth:search_for_state (which the
lifecycle hooks call) or query the underlying client directly.
Source code in src/locus/memory/managers/oracle_agent_memory.py
save
async
¶
Persist memories explicitly. Called by direct users; the
agent lifecycle path bypasses this in favour of
Thread.add_messages_async (which triggers the LLM
extractor).
Source code in src/locus/memory/managers/oracle_agent_memory.py
on_session_start
async
¶
Pull oracleagentmemory context for the current scope and inject it as a system-prompt block.
Two complementary signals:
- Per-thread context card — recent messages + summary of this thread (empty on the very first call to a brand-new thread).
- Cross-thread user-scoped memories — what the LLM
extractor has stored about this
user_idfrom prior threads. Critical for the "new conversation, same user" case: without this, a brand-new thread starts blank even though prior threads taught durable facts.
Source code in src/locus/memory/managers/oracle_agent_memory.py
on_session_end
async
¶
Ship the conversation to the underlying Thread so the
client's extractor can run.
Source code in src/locus/memory/managers/oracle_agent_memory.py
retrieve_relevant
async
¶
Retrieve the memories most relevant to query.
Called by :meth:on_session_start with the text of the current user
turn. The default ignores query and delegates to :meth:retrieve,
so subclasses written against the older retrieve(limit) contract
keep working unchanged; managers whose backend can rank by relevance
override this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str | None
|
Text to rank memories against, usually the latest user
message. |
required |
limit
|
int | None
|
Maximum number of memories to return. |
None
|
Source code in src/locus/memory/manager.py
drain
async
¶
Wait for every scheduled background extraction to finish.
Jobs submitted while draining are waited for too. Safe to call in
"inline" mode (returns at once). Errors inside jobs were already
logged and emitted; they are not raised here. Bound the wait with
asyncio.timeout(...): cancelling drain leaves the jobs running.
Source code in src/locus/memory/manager.py
Mem0MemoryManager ¶
Mem0MemoryManager(*, client: Any = None, config: dict[str, Any] | None = None, user_id: str | None = None, agent_id: str | None = None, run_id: str | None = None, search_query: str | None = None, retrieve_limit: int = 20)
Bases: BaseMemoryManager
Memory manager backed by mem0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
Any
|
A pre-built mem0 async memory client (anything exposing
async |
None
|
config
|
dict[str, Any] | None
|
mem0 configuration dict passed to |
None
|
user_id / agent_id / run_id
|
mem0 scope identifiers. At least one
(typically |
required | |
search_query
|
str | None
|
Optional query used by :meth: |
None
|
retrieve_limit
|
int
|
Maximum memories returned by :meth: |
20
|
Source code in src/locus/memory/managers/mem0.py
pending_extractions
property
¶
Background extractions scheduled or running, not yet finished.
extract
async
¶
retrieve
async
¶
Retrieve in-scope memories from mem0 for injection.
Source code in src/locus/memory/managers/mem0.py
save
async
¶
Persist explicit memories to mem0 (used for direct save calls).
Source code in src/locus/memory/managers/mem0.py
on_session_end
async
¶
Hand the finished conversation to mem0 for extraction + storage.
Source code in src/locus/memory/managers/mem0.py
retrieve_relevant
async
¶
Retrieve the memories most relevant to query.
Called by :meth:on_session_start with the text of the current user
turn. The default ignores query and delegates to :meth:retrieve,
so subclasses written against the older retrieve(limit) contract
keep working unchanged; managers whose backend can rank by relevance
override this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str | None
|
Text to rank memories against, usually the latest user
message. |
required |
limit
|
int | None
|
Maximum number of memories to return. |
None
|
Source code in src/locus/memory/manager.py
on_session_start
async
¶
Retrieve memories and inject them into the agent state.
Called by the agent runtime at the start of every invocation,
after on_before_invocation hooks but before the first model
call. The latest user message is used as the retrieval query, and
the formatted block replaces any block injected by an earlier turn
of the same (checkpointed) thread instead of being added next to it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
AgentState
|
Current agent state (just-created or loaded from checkpointer). |
required |
Returns:
| Type | Description |
|---|---|
AgentState
|
Possibly-modified state with memory context injected. |
Source code in src/locus/memory/manager.py
drain
async
¶
Wait for every scheduled background extraction to finish.
Jobs submitted while draining are waited for too. Safe to call in
"inline" mode (returns at once). Errors inside jobs were already
logged and emitted; they are not raised here. Bound the wait with
asyncio.timeout(...): cancelling drain leaves the jobs running.
Source code in src/locus/memory/manager.py
Delta checkpointing¶
Storage-efficient checkpointer that persists only the diff between
consecutive states (the module targets ~77% storage reduction on long
conversations; actual savings depend on workload). Layered on top of
any DeltaStorage backend.
DeltaCheckpointer ¶
DeltaCheckpointer(storage: DeltaStorage | None = None, max_chain_depth: int = 5, compression_level: int = 6)
Delta-based checkpointer for efficient state persistence.
This checkpointer stores only the differences between states, achieving significant storage savings. It maintains a chain of deltas pointing back to a full checkpoint.
Features: - Delta compression: Only store changes from parent state - Chain depth limiting: Force full checkpoint after N deltas - zlib compression: Additional compression of stored data - ~77% storage reduction in typical usage
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
storage
|
DeltaStorage | None
|
Storage backend for checkpoints |
None
|
max_chain_depth
|
int
|
Maximum number of deltas before forcing full checkpoint |
5
|
compression_level
|
int
|
zlib compression level (0-9, higher = better compression) |
6
|
Source code in src/locus/memory/delta.py
save
async
¶
Save agent state with delta compression.
If a parent checkpoint exists and chain depth allows, only the delta from parent is stored. Otherwise, a full checkpoint is created.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
AgentState
|
Current agent state |
required |
thread_id
|
str
|
Thread identifier |
required |
checkpoint_id
|
str | None
|
Optional specific checkpoint ID |
None
|
Returns:
| Type | Description |
|---|---|
str
|
Checkpoint ID for the saved state |
Source code in src/locus/memory/delta.py
load
async
¶
Load agent state from checkpoint.
If loading a delta checkpoint, reconstructs full state by walking the delta chain back to a full checkpoint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
thread_id
|
str
|
Thread identifier |
required |
checkpoint_id
|
str | None
|
Specific checkpoint ID (latest if None) |
None
|
Returns:
| Type | Description |
|---|---|
AgentState | None
|
Restored AgentState or None if not found |
Source code in src/locus/memory/delta.py
list_checkpoints
async
¶
List available checkpoint IDs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
thread_id
|
str
|
Thread identifier |
required |
limit
|
int
|
Maximum number to return |
10
|
Returns:
| Type | Description |
|---|---|
list[str]
|
List of checkpoint IDs, newest first |
Source code in src/locus/memory/delta.py
get_metadata
async
¶
Get metadata for a specific checkpoint.
Source code in src/locus/memory/delta.py
delete
async
¶
Delete checkpoint(s).
Source code in src/locus/memory/delta.py
get_storage_stats
async
¶
Get storage statistics for a thread.
Returns dict with: - total_checkpoints: Number of checkpoints - total_size: Uncompressed size - compressed_size: Compressed size - compression_ratio: Overall compression ratio - full_checkpoints: Number of full checkpoints - delta_checkpoints: Number of delta checkpoints
Source code in src/locus/memory/delta.py
DeltaCheckpoint
dataclass
¶
A delta checkpoint containing changes from parent.
compression_ratio
property
¶
Calculate compression ratio (uncompressed / compressed).
CheckpointMetadata
dataclass
¶
CheckpointMetadata(checkpoint_id: str, thread_id: str, parent_id: str | None = None, is_full: bool = True, chain_depth: int = 0, created_at: datetime = (lambda: datetime.now(UTC))(), size_bytes: int = 0, compressed_size_bytes: int = 0)
Metadata for a checkpoint.
DeltaStorage ¶
InMemoryDeltaStorage ¶
Bases: DeltaStorage
In-memory storage for delta checkpoints (for testing).
Source code in src/locus/memory/delta.py
Registry¶
String-based checkpointer lookup — used when configuration passes a
provider name (e.g. "s3", "redis") instead of an instance.
Custom backends register themselves via register_checkpointer.
get_checkpointer ¶
Get a checkpointer from a string identifier.
Format: "provider" or "provider:config_hint"
The config_hint is provider-specific and is passed as a keyword argument.
Examples:
- "memory" -> MemoryCheckpointer
- "file:./checkpoints" -> FileCheckpointer(base_dir="./checkpoints")
- "redis:localhost:6379" -> RedisCheckpointer(url="redis://localhost:6379")
- "postgresql:mydb" -> PostgreSQLCheckpointer(database="mydb")
- "mysql:mydb" -> MySQLCheckpointer(database="mydb")
- "opensearch" -> OpenSearchCheckpointer()
- "s3:my-bucket" -> S3Backend(bucket="my-bucket")
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
checkpointer_string
|
str
|
Checkpointer identifier |
required |
**kwargs
|
Any
|
Provider-specific configuration |
{}
|
Returns:
| Type | Description |
|---|---|
BaseCheckpointer
|
Checkpointer instance |
Raises:
| Type | Description |
|---|---|
ValueError
|
If provider is unknown |
Source code in src/locus/memory/registry.py
register_checkpointer ¶
Register a checkpointer provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Provider name (e.g., "redis", "postgresql", "s3") |
required |
factory
|
Callable[..., BaseCheckpointer]
|
Factory function that takes kwargs and returns a checkpointer |
required |
Example
def my_factory(kwargs) -> BaseCheckpointer: ... return MyCustomCheckpointer(kwargs) register_checkpointer("custom", my_factory)
Source code in src/locus/memory/registry.py
list_checkpointers ¶
List available checkpointer providers.
Returns:
| Type | Description |
|---|---|
list[str]
|
List of registered provider names |