zep-strands
Add long-term agent memory to Strands Agents via Zep's temporal Context Graph.
ZepMemoryStore implements Strands' MemoryStore interface, so it plugs into MemoryManager for automatic recall (injection + search_memory), server-side extraction (add_messages → Zep threads), and optional on-demand graph search.
Installation
pip install zep-strands
Requires Python 3.11+, strands-agents>=1.45.0, zep-cloud>=3.23.0, and a Zep Cloud API key from app.getzep.com.
Quick start
from strands import Agent
from strands.memory import MemoryManager
from zep_cloud.client import AsyncZep
from zep_strands import ZepMemoryStore, ensure_thread, ensure_user
zep = AsyncZep(api_key="your-api-key")
await ensure_user(
zep,
user_id="user-123",
first_name="Jane",
last_name="Smith",
email="jane@example.com",
)
await ensure_thread(zep, thread_id="thread-abc", user_id="user-123")
store = ZepMemoryStore(
zep_client=zep,
user_id="user-123",
thread_id="thread-abc",
first_name="Jane",
last_name="Smith",
writable=True,
extraction=True, # server-side via add_messages
)
agent = Agent(
system_prompt="You are a helpful assistant with long-term memory.",
memory_manager=MemoryManager(stores=[store]),
)
With no further configuration, the manager injects relevant Zep context before each user turn and runs server-side extraction on Strands' default cadence (every 5 turns). That means conversation turns are buffered and only sent to Zep when the trigger fires (or when you call memory_manager.flush()), so graph building is delayed relative to turn-by-turn persistence — and Zep's own ingestion remains asynchronous after messages arrive. Enable add_tool_config=True on the manager to also let the model call add_memory.
How it works
| Strands hook | Zep call | Purpose |
|---|---|---|
MemoryStore.search |
graph.search |
Recall for injection and the search_memory tool |
MemoryStore.add_messages |
thread.add_messages |
Server-side extraction from conversation turns |
MemoryStore.add |
graph.add |
Single-fact writes (add_memory tool / programmatic) |
First search / write |
user.add + thread.create |
Provision resources on first use (see below) |
MemoryStore.get_tools |
create_zep_search_tool |
Optional on-demand graph search (when enabled) |
Context comes from the whole user graph; the thread only scopes relevance and records the conversation. A new thread for the same user still recalls earlier facts.
Automatic extraction and delayed graph building
extraction=True (the default when the store is writable with user_id + thread_id) opts into Strands' automatic extraction loop. With the manager's defaults that means:
- Conversation turns are buffered in the manager.
- Every 5 turns, Strands calls
add_messages, which posts the batch to Zep viathread.add_messages. - Zep then processes the batch asynchronously into the user graph.
Until step 2 runs, nothing has been sent to Zep, so the graph does not grow turn-by-turn. After step 2, facts are still not instantly searchable (Zep ingestion is async). Plan for both delays:
- Call
await memory_manager.flush()at session boundaries (required afterinvoke_async/stream_asyncif you need pending turns persisted before shutdown). - Or pass an every-turn trigger if you need messages sent to Zep more often:
from strands.memory.extraction.triggers import InvocationTrigger
from strands.memory.extraction.types import ExtractionConfig
store = ZepMemoryStore(
zep_client=zep,
user_id="user-123",
thread_id="thread-abc",
writable=True,
extraction=ExtractionConfig(trigger=InvocationTrigger()), # after every turn
)
extraction=True (or an ExtractionConfig) requires writable user-graph mode with both user_id and thread_id. Construction raises ValueError otherwise — use extraction=False for standalone graphs or read-only stores.
Scoping modes
User graph (default for conversational agents) — pass user_id and thread_id:
ZepMemoryStore(zep_client=zep, user_id="user-123", thread_id="thread-abc", ...)
Standalone graph (shared / domain knowledge) — pass graph_id. Supports search and add only (no add_messages):
ZepMemoryStore(zep_client=zep, graph_id="company-kb", writable=True, extraction=False)
Provide exactly one of user_id or graph_id.
Provisioning users and threads
ensure_user / ensure_thread are idempotent create-then-catch-conflict helpers. Both return True when newly created and False when the resource already exists; genuine failures raise.
Call them out-of-band before the first turn so misconfiguration surfaces loudly. If you skip them, the store provisions itself on its first search or write instead.
ZepMemoryStore.initialize() deliberately makes no Zep calls. Agent.__init__ is synchronous, so Strands runs that hook on a throwaway event loop in a worker thread; calling Zep there would drive your AsyncZep client from a second event loop and raise RuntimeError: ... is bound to a different event loop for any connection you had already opened. Deferring to first use keeps every Zep call on the agent's own loop, so one client can safely be shared between your application code and the store.
from zep_strands import ensure_thread, ensure_user
created = await ensure_user(
zep,
user_id="user-123",
first_name="Jane",
last_name="Smith",
on_created=configure_ontology, # optional async hook
)
await ensure_thread(zep, thread_id="thread-abc", user_id="user-123")
Search and injection
By default search_scope="auto", so injection receives Zep's assembled Context Block as a single MemoryEntry. Pin a scoped search when you want discrete facts:
store = ZepMemoryStore(
zep_client=zep,
user_id="user-123",
thread_id="thread-abc",
search_scope="edges",
search_filters={"edge_types": ["PREFERS"]},
)
On-demand graph search tool
Set expose_search_tool=True to register a model-callable zep_search tool via get_tools():
store = ZepMemoryStore(
zep_client=zep,
user_id="user-123",
thread_id="thread-abc",
expose_search_tool=True,
search_pinned_params={"scope": "auto", "limit": 10},
)
Or build the tool yourself:
from zep_strands import create_zep_search_tool
tool = create_zep_search_tool(
zep_client=zep,
user_id="user-123",
search_pinned_params={"scope": "edges"},
)
agent = Agent(tools=[tool])
Pin-or-expose. Every graph.search parameter (scope, reranker, limit, mmr_lambda, center_node_uuid) is model-exposed by default. search_pinned_params fixes a value and hides it; search_hidden_params hides without pinning (Zep's default applies). search_filters and bfs_origin_node_uuids are always constructor-only.
Writing facts
# Text fact into the user graph
await store.add("Prefers aisle seats", metadata={"source": "prefs"})
# JSON payload
await store.add('{"plan": "premium"}', metadata={"type": "json"})
metadata["type"] selects the Zep data type (text default, json, or message). Remaining metadata keys are forwarded as episode metadata.
Oversized text/message payloads are truncated to Zep's graph.add limit with a warning. Oversized json is rejected with a ValueError instead — slicing JSON strips its closing syntax, so a truncated document would just be rejected by Zep. Split large JSON into smaller documents before adding (see chunking).
Error handling
Zep SDK errors propagate out of the store methods deliberately: in Strands the framework owns failure isolation, and swallowing them breaks it.
| Path | Framework behavior on a raise |
|---|---|
search |
MemoryManager.search logs and skips the failing store; injection additionally fails open, so the turn proceeds without memory |
add |
Surfaced as AggregateMemoryError so a failed write is never silent |
add_messages |
ExtractionCoordinator catches it and rolls back its high-water mark so the batch retries |
That last one matters most: returning None after swallowing an error would be read as success, advancing the mark and discarding those messages permanently. This matches the SDK's own vended stores, which raise rather than degrade.
The one exception is the model-callable zep_search tool, which catches Zep errors and returns an error string — a raw tool has no framework layer above it.
Identity
Pass real names (first_name, last_name, email) so Zep anchors the user graph node. Display names on persisted messages default to the user's full name / "Assistant".
One store instance is bound to one user_id/thread_id (or graph_id) at construction.
Features
- Native Strands
MemoryStore— works withMemoryManagerinjection, tools, and extraction - Server-side extraction via Zep threads (
add_messages); default cadence every 5 turns (delayed graph building) - Fail-fast validation when
extractionis enabled without a writable user/thread - Whole-user-graph recall across threads
- Standalone-graph mode for shared knowledge
- Optional pin-or-expose
zep_searchtool - Idempotent
ensure_user/ensure_threadprovisioning - Message and graph payload truncation with length-only warnings
Configuration
export ZEP_API_KEY="your-zep-api-key"
Examples
See examples/basic_agent.py for an end-to-end multi-thread recall demo. Setup steps are in SETUP.md.
Development
cd integrations/strands/python
make install # uv sync --extra dev
make all # format + lint + type-check + test
Requirements
- Python 3.11+
strands-agents>=1.45.0zep-cloud>=3.23.0
Support
License
Apache 2.0 — see the repository LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zep_strands-0.1.0.tar.gz.
File metadata
- Download URL: zep_strands-0.1.0.tar.gz
- Upload date:
- Size: 33.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19dda2d3e942b6ae22c77ba1607f94f3e5f7c3efd1df38ebfb54c61eb50d537d
|
|
| MD5 |
4748e2df72a1f76b61f62a4229b8e24f
|
|
| BLAKE2b-256 |
36351bd31eb45abacfee996b2ae8a7570c9234d831865ed21e2c01acd349ec4f
|
Provenance
The following attestation bundles were made for zep_strands-0.1.0.tar.gz:
Publisher:
release-integrations.yml on getzep/zep
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zep_strands-0.1.0.tar.gz -
Subject digest:
19dda2d3e942b6ae22c77ba1607f94f3e5f7c3efd1df38ebfb54c61eb50d537d - Sigstore transparency entry: 2372642709
- Sigstore integration time:
-
Permalink:
getzep/zep@cacd16335d6b3251c5e3b10118272bedaf32e45e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/getzep
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-integrations.yml@cacd16335d6b3251c5e3b10118272bedaf32e45e -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file zep_strands-0.1.0-py3-none-any.whl.
File metadata
- Download URL: zep_strands-0.1.0-py3-none-any.whl
- Upload date:
- Size: 23.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c3fcaf669b126929c774b31a7f93550a3debc2f088a2c6d4e08846f58bf9d2e
|
|
| MD5 |
4c46bb610bb0a155dc2045bdd488d1a3
|
|
| BLAKE2b-256 |
0bb9f749aec9a37c9e94d38aa7651524b1d2efb421d5ef3e4f3ba00b2d5dbe8c
|
Provenance
The following attestation bundles were made for zep_strands-0.1.0-py3-none-any.whl:
Publisher:
release-integrations.yml on getzep/zep
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zep_strands-0.1.0-py3-none-any.whl -
Subject digest:
0c3fcaf669b126929c774b31a7f93550a3debc2f088a2c6d4e08846f58bf9d2e - Sigstore transparency entry: 2372642743
- Sigstore integration time:
-
Permalink:
getzep/zep@cacd16335d6b3251c5e3b10118272bedaf32e45e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/getzep
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-integrations.yml@cacd16335d6b3251c5e3b10118272bedaf32e45e -
Trigger Event:
workflow_dispatch
-
Statement type: