Skip to content

Commit f92ac87

Browse files
mehulp93cursoragent
andcommitted
fix(strands): keep example on default extraction cadence
Use production-default extraction=True with a session-boundary flush instead of an every-turn InvocationTrigger in the example and live tests. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 77ee650 commit f92ac87

3 files changed

Lines changed: 53 additions & 44 deletions

File tree

integrations/strands/python/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121

2222
- 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.
2323
- 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.
24+
- Example and live tests flush at the session boundary after `invoke_async`,
25+
which `MemoryManager` requires to persist buffered turns on that path.
2626

2727
## 0.1.0 (2026-08-01)
2828

integrations/strands/python/examples/basic_agent.py

Lines changed: 35 additions & 35 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-
Wires a ``ZepMemoryStore`` into Strands' ``MemoryManager`` so the agent:
4+
This example wires a ``ZepMemoryStore`` into Strands' ``MemoryManager`` so the
5+
agent automatically:
56
67
* injects relevant Zep context before each model call
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
8+
* extracts conversation turns into the user graph (server-side, via
9+
``add_messages``) on the manager's default cadence
1010
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.
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.
1313
1414
Prerequisites:
1515
pip install zep-strands 'strands-agents[openai]'
@@ -26,13 +26,27 @@
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
3129
from strands.types.content import Message
3230
from zep_cloud.client import AsyncZep
3331

3432
from zep_strands import ZepMemoryStore, ensure_thread, ensure_user
3533

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+
# ---------------------------------------------------------------------------
3650
ZEP_API_KEY = os.environ.get("ZEP_API_KEY", "")
3751
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
3852

@@ -47,18 +61,6 @@
4761
THREAD_2 = f"strands-example-thread2-{_suffix}"
4862

4963

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-
6264
async def build_agent(zep: AsyncZep, thread_id: str) -> Agent:
6365
"""Build an agent whose memory is scoped to USER_ID on the given thread."""
6466
await ensure_user(
@@ -78,9 +80,7 @@ async def build_agent(zep: AsyncZep, thread_id: str) -> Agent:
7880
last_name="Nguyen",
7981
email="alice@example.com",
8082
writable=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()),
83+
extraction=True,
8484
expose_search_tool=True,
8585
search_pinned_params={"scope": "auto"},
8686
)
@@ -94,14 +94,6 @@ 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-
10597
async def main() -> None:
10698
zep = AsyncZep(api_key=ZEP_API_KEY)
10799

@@ -115,12 +107,19 @@ async def main() -> None:
115107

116108
print("--- Conversation 1: seeding facts ---\n")
117109
agent1 = await build_agent(zep, THREAD_1)
118-
for message in (
110+
seed_messages = [
119111
"Hi! I'm Alice, a data scientist living in Portland, Oregon.",
120112
"On weekends I love hiking and landscape photography.",
121-
):
113+
]
114+
for message in seed_messages:
122115
print(f"User: {message}")
123-
print(f"Agent: {await chat(agent1, message)}\n")
116+
result = await agent1.invoke_async(message)
117+
print(f"Agent: {_message_text(result.message)}\n")
118+
119+
# Flush at the session boundary: invoke_async does not flush, and flushing
120+
# every turn would defeat the extraction trigger's schedule.
121+
if agent1.memory_manager is not None:
122+
await agent1.memory_manager.flush()
124123

125124
wait_seconds = 20
126125
print(f"--- Waiting {wait_seconds}s for Zep to process the graph ---\n")
@@ -130,7 +129,8 @@ async def main() -> None:
130129
agent2 = await build_agent(zep, THREAD_2)
131130
recall = "Where do I live, and what do I like to do on weekends?"
132131
print(f"User: {recall}")
133-
print(f"Agent: {await chat(agent2, recall)}\n")
132+
result = await agent2.invoke_async(recall)
133+
print(f"Agent: {_message_text(result.message)}\n")
134134

135135

136136
if __name__ == "__main__":

integrations/strands/python/tests/test_integration.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@
4545

4646
from strands import Agent # noqa: E402
4747
from strands.memory import MemoryManager # noqa: E402
48-
from strands.memory.extraction.triggers import InvocationTrigger # noqa: E402
49-
from strands.memory.extraction.types import ExtractionConfig # noqa: E402
5048
from strands.types.content import Message # noqa: E402
5149
from zep_cloud.client import AsyncZep # noqa: E402
5250

@@ -129,8 +127,8 @@ async def build_agent(
129127
) -> Agent:
130128
"""Build an agent whose memory is scoped to USER_ID on the given thread.
131129
132-
Uses every-turn extraction so the test is not flaky on Strands' default
133-
5-turn cadence; callers still ``flush()`` after ``invoke_async``.
130+
Configured exactly as the example is: default extraction cadence, with
131+
callers flushing at the session boundary.
134132
"""
135133
await ensure_user(
136134
zep,
@@ -150,7 +148,7 @@ async def build_agent(
150148
last_name=LAST_NAME,
151149
email=EMAIL,
152150
writable=True,
153-
extraction=ExtractionConfig(trigger=InvocationTrigger()),
151+
extraction=True,
154152
expose_search_tool=True,
155153
search_pinned_params={"scope": "auto"},
156154
)
@@ -165,11 +163,19 @@ async def build_agent(
165163

166164

167165
async def chat(agent: Agent, message: str) -> str:
168-
"""Send one message and flush pending extraction writes."""
166+
"""Send one message to the agent and return its text reply."""
169167
result = await agent.invoke_async(message)
168+
return _message_text(result.message)
169+
170+
171+
async def flush(agent: Agent) -> None:
172+
"""Force buffered extraction writes out at a session boundary.
173+
174+
``invoke_async`` never flushes on its own, and the default trigger only
175+
fires every 5 turns, so without this the seeded turns never reach Zep.
176+
"""
170177
if agent.memory_manager is not None:
171178
await agent.memory_manager.flush()
172-
return _message_text(result.message)
173179

174180

175181
# ---------------------------------------------------------------------------
@@ -261,6 +267,7 @@ async def on_user_created(client: AsyncZep, user_id: str) -> None:
261267
agent1, "My name is IntegTest. I work at Acme Corp as a data scientist."
262268
)
263269
reply2 = await chat(agent1, "I live in Portland, Oregon and I love hiking and photography.")
270+
await flush(agent1)
264271
assert reply1
265272
assert reply2
266273
assert hook_calls == [USER_ID]
@@ -325,6 +332,8 @@ async def on_user_created(client: AsyncZep, user_id: str) -> None:
325332
print(f" Agent: {reply}\n")
326333
passed &= check("Agent returned a non-empty response", len(reply) > 0)
327334

335+
await flush(agent1)
336+
328337
passed &= check(
329338
"on_user_created hook fired exactly once",
330339
len(hook_calls) == 1 and hook_calls[0] == USER_ID,

0 commit comments

Comments
 (0)