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
tulip.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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/conversation.py
122 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 | |
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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/conversation.py
Cross-thread store¶
The cross-thread KV the long-term memory manager writes to. Three 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 |
The drivers in tulip.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 .sdk/src/tulip/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 .sdk/src/tulip/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:~tulip.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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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:~tulip.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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/store.py
get_user_memory
async
¶
Get a memory for the current user.
remember
async
¶
Store a memory for the current user.
Source code in .sdk/src/tulip/memory/store.py
forget
async
¶
search_memories
async
¶
Search user memories (full-text).
Source code in .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/store.py
from_dict
classmethod
¶
Create from dictionary.
Source code in .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/store_backends/holographic.py
exists
async
¶
get_item
async
¶
Get full item with metadata.
Source code in .sdk/src/tulip/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:~tulip.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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/store.py
PgMemory ¶
PgMemory(dsn: str, *, table: str = 'tulip_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:~tulip.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 .sdk/src/tulip/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 .sdk/src/tulip/memory/store_backends/postgresql.py
exists
async
¶
get_item
async
¶
Get full item with metadata.
Source code in .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/store.py
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 "tulip-agents[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.
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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/manager.py
LLMMemoryManager ¶
LLMMemoryManager(store: BaseStore, *, extract_fn: ExtractFn | None = None, namespace_prefix: tuple[str, ...] = ('tulip_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:~tulip.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 |
('tulip_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 tulip.memory.manager import LLMMemoryManager
from tulip.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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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:~tulip.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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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.
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 .sdk/src/tulip/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 .sdk/src/tulip/memory/managers/mem0.py
save
async
¶
Persist explicit memories to mem0 (used for direct save calls).
Source code in .sdk/src/tulip/memory/managers/mem0.py
on_session_end
async
¶
Hand the finished conversation to mem0 for extraction + storage.
Source code in .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/delta.py
get_metadata
async
¶
Get metadata for a specific checkpoint.
Source code in .sdk/src/tulip/memory/delta.py
delete
async
¶
Delete checkpoint(s).
Source code in .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/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 .sdk/src/tulip/memory/registry.py
list_checkpointers ¶
List available checkpointer providers.
Returns:
| Type | Description |
|---|---|
list[str]
|
List of registered provider names |