Skip to content

Commit 77ee650

Browse files
mehulp93cursoragent
andcommitted
test(strands): add live agent E2E and polish for public release
Bring zep-strands to peer parity: full Agent+MemoryManager integration test (gated on ZEP+OPENAI), root README listing, and example/docs cleanup. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ab76c12 commit 77ee650

6 files changed

Lines changed: 417 additions & 80 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Framework integration packages live under [`integrations/`](integrations/), orga
4949
framework-first then language: `integrations/<framework>/<language>/`. Each package is built,
5050
tested, and released independently.
5151

52-
- **Python**: Google ADK, Microsoft Agent Framework, Microsoft AutoGen, AG2, CrewAI, LangGraph, LiveKit, Pydantic AI
52+
- **Python**: Google ADK, Microsoft Agent Framework, Microsoft AutoGen, AG2, CrewAI, LangGraph, LiveKit, Pydantic AI, Strands Agents
5353
- **TypeScript**: Google ADK, Mastra, Vercel AI SDK
5454
- **Go**: Google ADK
5555

integrations/strands/python/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,27 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- Live agent integration test (`test_integration_full_lifecycle`) exercising
8+
`Agent` + `MemoryManager` + `ZepMemoryStore` against Zep Cloud and OpenAI,
9+
including cross-thread recall and `on_user_created` (gated on
10+
`ZEP_API_KEY` + `OPENAI_API_KEY`). Store-only round-trip remains available
11+
with just `ZEP_API_KEY`.
12+
513
### Fixed
614

715
- `ZepMemoryStore` now rejects `extraction=True` (or an `ExtractionConfig`) at construction unless the store is writable user-graph mode with both `user_id` and `thread_id`, so `MemoryManager` never schedules extraction that would raise on every cycle.
816
- `add()` no longer truncates oversized `json` payloads. Slicing JSON strips its closing syntax, so the size guard produced a document Zep would reject; oversized `json` now raises a `ValueError` pointing at chunking. `text`/`message` payloads are still truncated with a warning.
17+
- Corrected the `provisioning` module docstring (it incorrectly referred to
18+
`ZepContextProvider`).
919

1020
### Changed
1121

1222
- Documented that Strands' default extraction cadence is every **5 turns**, so conversation batches only reach Zep when the trigger fires (or on `flush()`), delaying graph building relative to turn-by-turn persistence — in addition to Zep's asynchronous ingestion after messages arrive.
1323
- Documented the failure-handling contract: Zep SDK errors propagate out of `search`/`add`/`add_messages` by design, because `MemoryManager` and `ExtractionCoordinator` own failure isolation (skip-and-log, `AggregateMemoryError`, and high-water-mark rollback for retry). Added tests pinning that behavior.
24+
- Example and live tests use every-turn extraction (`InvocationTrigger`) plus
25+
`flush()` after `invoke_async` so demos are not flaky on the default cadence.
1426

1527
## 0.1.0 (2026-08-01)
1628

integrations/strands/python/SETUP.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,15 @@ Mock-based tests (no API keys needed):
7272
make test
7373
```
7474

75-
Live integration test (requires `ZEP_API_KEY`):
75+
Live integration tests:
7676

7777
```bash
78+
# Store round-trip (ZEP_API_KEY only)
79+
# Full agent lifecycle (also needs OPENAI_API_KEY)
7880
uv run pytest tests/test_integration.py -v -s -m integration
81+
82+
# Or run the agent lifecycle as a standalone script:
83+
uv run python tests/test_integration.py
7984
```
8085

8186
## Troubleshooting

integrations/strands/python/examples/basic_agent.py

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
"""
22
Basic Strands Agents agent with Zep long-term memory via MemoryManager.
33
4-
This example wires a ``ZepMemoryStore`` into Strands' ``MemoryManager`` so the
5-
agent automatically:
4+
Wires a ``ZepMemoryStore`` into Strands' ``MemoryManager`` so the agent:
65
76
* injects relevant Zep context before each model call
8-
* extracts conversation turns into the user graph (server-side, via
9-
``add_messages``) on the manager's default cadence
7+
* extracts conversation turns into the user graph (server-side via
8+
``add_messages``) on an every-turn trigger, then ``flush()`` after each
9+
``invoke_async`` so nothing is left buffered
1010
11-
Earlier turns seed facts about the user; a later turn -- in a *new* conversation
12-
thread -- shows the agent recalling those facts from Zep's user graph.
11+
Earlier turns seed facts about the user; a later turn on a *new* thread shows
12+
cross-session recall from Zep's user graph.
1313
1414
Prerequisites:
1515
pip install zep-strands 'strands-agents[openai]'
@@ -26,27 +26,13 @@
2626

2727
from strands import Agent
2828
from strands.memory import MemoryManager
29+
from strands.memory.extraction.triggers import InvocationTrigger
30+
from strands.memory.extraction.types import ExtractionConfig
2931
from strands.types.content import Message
3032
from zep_cloud.client import AsyncZep
3133

3234
from zep_strands import ZepMemoryStore, ensure_thread, ensure_user
3335

34-
35-
def _message_text(message: Message | object) -> str:
36-
"""Extract joined text blocks from an agent result message."""
37-
if not isinstance(message, dict):
38-
return str(message)
39-
parts = [
40-
block["text"]
41-
for block in message.get("content") or []
42-
if isinstance(block, dict) and "text" in block
43-
]
44-
return "\n".join(parts) if parts else str(message)
45-
46-
47-
# ---------------------------------------------------------------------------
48-
# Configuration
49-
# ---------------------------------------------------------------------------
5036
ZEP_API_KEY = os.environ.get("ZEP_API_KEY", "")
5137
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
5238

@@ -61,6 +47,18 @@ def _message_text(message: Message | object) -> str:
6147
THREAD_2 = f"strands-example-thread2-{_suffix}"
6248

6349

50+
def _message_text(message: Message | object) -> str:
51+
"""Extract joined text blocks from an agent result message."""
52+
if not isinstance(message, dict):
53+
return str(message)
54+
parts = [
55+
block["text"]
56+
for block in message.get("content") or []
57+
if isinstance(block, dict) and "text" in block
58+
]
59+
return "\n".join(parts) if parts else str(message)
60+
61+
6462
async def build_agent(zep: AsyncZep, thread_id: str) -> Agent:
6563
"""Build an agent whose memory is scoped to USER_ID on the given thread."""
6664
await ensure_user(
@@ -80,7 +78,9 @@ async def build_agent(zep: AsyncZep, thread_id: str) -> Agent:
8078
last_name="Nguyen",
8179
email="alice@example.com",
8280
writable=True,
83-
extraction=True,
81+
# Every-turn extraction so the demo does not wait on the default
82+
# 5-turn cadence; still flush after invoke_async (see main).
83+
extraction=ExtractionConfig(trigger=InvocationTrigger()),
8484
expose_search_tool=True,
8585
search_pinned_params={"scope": "auto"},
8686
)
@@ -94,6 +94,14 @@ async def build_agent(zep: AsyncZep, thread_id: str) -> Agent:
9494
)
9595

9696

97+
async def chat(agent: Agent, message: str) -> str:
98+
"""Send one message and flush pending extraction writes."""
99+
result = await agent.invoke_async(message)
100+
if agent.memory_manager is not None:
101+
await agent.memory_manager.flush()
102+
return _message_text(result.message)
103+
104+
97105
async def main() -> None:
98106
zep = AsyncZep(api_key=ZEP_API_KEY)
99107

@@ -107,17 +115,12 @@ async def main() -> None:
107115

108116
print("--- Conversation 1: seeding facts ---\n")
109117
agent1 = await build_agent(zep, THREAD_1)
110-
seed_messages = [
118+
for message in (
111119
"Hi! I'm Alice, a data scientist living in Portland, Oregon.",
112120
"On weekends I love hiking and landscape photography.",
113-
]
114-
for message in seed_messages:
121+
):
115122
print(f"User: {message}")
116-
result = await agent1.invoke_async(message)
117-
# Flush pending extraction writes before moving on.
118-
if agent1.memory_manager is not None:
119-
await agent1.memory_manager.flush()
120-
print(f"Agent: {_message_text(result.message)}\n")
123+
print(f"Agent: {await chat(agent1, message)}\n")
121124

122125
wait_seconds = 20
123126
print(f"--- Waiting {wait_seconds}s for Zep to process the graph ---\n")
@@ -127,8 +130,7 @@ async def main() -> None:
127130
agent2 = await build_agent(zep, THREAD_2)
128131
recall = "Where do I live, and what do I like to do on weekends?"
129132
print(f"User: {recall}")
130-
result = await agent2.invoke_async(recall)
131-
print(f"Agent: {_message_text(result.message)}\n")
133+
print(f"Agent: {await chat(agent2, recall)}\n")
132134

133135

134136
if __name__ == "__main__":

integrations/strands/python/src/zep_strands/provisioning.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""
22
Explicit, out-of-band Zep resource provisioning.
33
4-
``ZepContextProvider``'s lazy call into these helpers (see
5-
``ZepContextProvider._ensure_resources``) is hot-path-wrapped and will never
6-
raise into an agent run. Callers who want provisioning failures (and
4+
``ZepMemoryStore.initialize`` (and its lazy ``_ensure_resources_lazy`` path)
5+
calls these helpers on the hot path and is wrapped so a Zep outage never
6+
raises into an agent turn. Callers who want provisioning failures (and
77
``on_created`` hook failures) to surface loudly -- e.g. during account/session
88
onboarding, before the first turn -- should call :func:`ensure_user` and
99
:func:`ensure_thread` directly, out-of-band.

0 commit comments

Comments
 (0)