-
Notifications
You must be signed in to change notification settings - Fork 134
feat(memory): add AgentCoreMemoryStore Strands integration #588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
strandly-the-agent
wants to merge
6
commits into
aws:main
Choose a base branch
from
strandly-the-agent:port/agentcore-memory-store-to-python
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8a9543e
feat(memory): port AgentCoreMemoryStore to Python
strandly bac5ffe
refactor(memory): address AgentCoreMemoryStore port review
strandly 56dcff8
fix(memory): preserve overfetch parity on overflow
strandly 0691f08
chore: merge main into AgentCoreMemoryStore port branch
strandly 8f2fac4
fix(memory): keep existing Strands integration imports untouched
strandly 8c06a2c
fix(memory): clamp search topK on every path
strandly File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
src/bedrock_agentcore/memory/integrations/strands/memorystore/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| # Strands AgentCore MemoryStore | ||
|
|
||
| `AgentCoreMemoryStore` plugs AgentCore long-term memory directly into Strands' `MemoryManager` for | ||
| long-term recall and extraction. It requires `strands-agents>=1.46.0`. | ||
|
|
||
| ## One namespace | ||
|
|
||
| A store is recall-only by default. Set `writable=True` on exactly one store when Strands should send | ||
| messages to AgentCore for server-side long-term extraction: | ||
|
|
||
| ```python | ||
| import os | ||
|
|
||
| from strands import Agent | ||
| from strands.memory import MemoryManager | ||
|
|
||
| from bedrock_agentcore.memory.integrations.strands.memorystore import AgentCoreMemoryStore | ||
|
|
||
| store = AgentCoreMemoryStore( | ||
| memory_id=os.environ["AGENTCORE_MEMORY_ID"], | ||
| actor_id="demo-user", | ||
| session_id="demo-session", | ||
| namespace="/facts/{actorId}/", | ||
| writable=True, | ||
| extraction=True, | ||
| region_name="us-east-1", | ||
| ) | ||
| manager = MemoryManager(stores=[store]) | ||
| agent = Agent(memory_manager=manager) | ||
| agent("Remember that I prefer window seats.") | ||
| ``` | ||
|
|
||
| `namespace` performs exact-prefix retrieval. Use `namespace_path` instead to search a namespace | ||
| subtree. The integration resolves `{actorId}` and `{sessionId}` client-side; substitute other | ||
| placeholders and malformed braces before constructing the store. | ||
|
|
||
| ## Multiple namespaces | ||
|
|
||
| `create_agentcore_memory_stores` returns `list[MemoryStore]` for direct `MemoryManager` composition. | ||
| Each item is a concrete `AgentCoreMemoryStore`; the factory shares one boto3 client and prevents | ||
| duplicate writes by allowing at most one writer: | ||
|
|
||
| ```python | ||
| import os | ||
|
|
||
| from strands.memory import IntervalTrigger, MemoryManager, MemoryMessageFilter | ||
|
|
||
| from bedrock_agentcore.memory.integrations.strands.memorystore import create_agentcore_memory_stores | ||
|
|
||
| stores = create_agentcore_memory_stores( | ||
| memory_id=os.environ["AGENTCORE_MEMORY_ID"], | ||
| actor_id="demo-user", | ||
| session_id="demo-session", | ||
| namespaces=[ | ||
| { | ||
| "namespace": "/preferences/{actorId}/", | ||
| "max_search_results": 5, | ||
| "min_score": 0.7, | ||
| }, | ||
| { | ||
| "namespace": "/facts/{actorId}/", | ||
| "max_search_results": 10, | ||
| "min_score": 0.3, | ||
| }, | ||
| ], | ||
| extraction={ | ||
| "cadence": IntervalTrigger(turns=10), | ||
| "filter": MemoryMessageFilter(exclude=["toolUse", "toolResult", "image"]), | ||
| }, | ||
| region_name="us-east-1", | ||
| ) | ||
| manager = MemoryManager(stores=stores) | ||
| ``` | ||
|
|
||
| With extraction enabled, the first namespace not explicitly marked `writable=False` becomes the | ||
| writer. Set `writable=True` on one namespace to choose it explicitly. Omit `extraction` or pass | ||
| `False` for recall-only stores. | ||
|
|
||
| ## Search and write behavior | ||
|
|
||
| - Search defaults to 5 results. `min_score` enables client-side score filtering and over-fetches by | ||
| a factor of 4 (configurable with `over_fetch_factor`); only the over-fetched `topK` is capped at 100. | ||
| - Returned metadata uses reserved keys `_id`, `_score`, `_namespaces`, and `_createdAt`. | ||
| - Writes preserve user/assistant roles, ignore blank and tool-only messages, and batch up to 50 | ||
| consecutive turns per AgentCore event by default. `max_turns_per_event` accepts any positive integer. | ||
| - `metadata_provider` returns scalar strings, finite numbers, or booleans. Strings pass through; | ||
| other finite scalars use Python `json.dumps` formatting. `None`, arrays, objects, non-finite numbers, | ||
| and values outside AgentCore's allowed character set are rejected locally. | ||
| - Direct `AgentCoreMemoryStore(...)` construction accepts `extraction_mode="SKIP"` to omit long-term | ||
| extraction for its events. The multi-namespace factory intentionally does not expose this option. | ||
| - `add_messages()` is the supported write interface. The flat-string Strands `add()` API is not | ||
| implemented because it loses role and turn information. | ||
|
|
||
| ## Batching, cadence, and flush | ||
|
|
||
| Three separate controls determine write timing and cost: | ||
|
|
||
| 1. **Batching is always on.** Each flush packs its role-tagged messages into as few `create_event` | ||
| requests as `max_turns_per_event` allows. | ||
| 2. **Cadence controls when buffered messages are dispatched across turns.** `extraction=True` uses | ||
| Strands' default trigger. Pass an extraction config with an `IntervalTrigger` or another Strands | ||
| trigger to tune cadence. | ||
| 3. **`flush()` lets pending write attempts settle; it does not acknowledge durability or server-side | ||
| extraction.** Strands 1.46 logs and swallows sender failures, rolls back its high-water mark, and | ||
| retains the failed batch for a later retry. | ||
|
|
||
| Synchronous `agent(...)` invocations flush automatically. After async invocation or streaming, call | ||
| `await manager.flush()` at a lifecycle or shutdown boundary to let pending writes settle. Monitor logs | ||
| or telemetry for failures rather than treating `flush()` as proof that data was persisted. AgentCore's | ||
| server-side extraction remains eventually consistent, so newly written records may not be immediately | ||
| searchable. | ||
|
|
||
| Reuse one manager per `(actor_id, session_id)` while that session is active. Reuse keeps trigger state | ||
| and buffered turns alive, allowing a coarser cadence to reduce calls. The application owns manager | ||
| caching and eviction. | ||
|
|
||
| ## Namespace and error contract | ||
|
|
||
| Recall works only when the query namespace matches the concrete namespace where AgentCore stored the | ||
| extracted record. Writes append to the shared `(memory_id, actor_id, session_id)` stream; the memory | ||
| resource's strategies decide which namespaces receive extracted records. That is why a store set must | ||
| have at most one writer. | ||
|
|
||
| - AgentCore resolves strategy placeholders at extraction time, but retrieval does not. The store | ||
| resolves only `{actorId}` and `{sessionId}` and rejects remaining braces at construction. | ||
| - Match the namespace template used when provisioning the strategy. `namespace` queries one exact | ||
| prefix; `namespace_path` queries a parent subtree. | ||
| - A namespace containing `{sessionId}` is session-scoped. Use a stable session id or actor-only | ||
| namespace for cross-session recall. | ||
| - The store consumes an existing memory resource; it does not provision strategies or the resource. | ||
| - Retrieval failures propagate to `MemoryManager`, which applies its per-store partial-failure behavior. | ||
| - Sender failures remain buffered for retry and are logged by Strands rather than propagated by | ||
| `flush()`. | ||
|
|
||
| The integration calls the boto3 `bedrock-agentcore` data-plane client directly. AWS credentials use | ||
| boto3's normal credential chain; no credentials are stored by the integration. |
40 changes: 40 additions & 0 deletions
40
src/bedrock_agentcore/memory/integrations/strands/memorystore/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| """Strands-native long-term memory stores backed by Bedrock AgentCore Memory.""" | ||
|
|
||
| from .factory import assert_writable_topology, create_agentcore_memory_stores | ||
| from .sender import AgentCoreEventSender | ||
| from .store import AgentCoreMemoryStore | ||
| from .types import ( | ||
| RESERVED_METADATA_PREFIX, | ||
| AgentCoreEventSenderConfig, | ||
| AgentCoreExactNamespaceStoreConfig, | ||
| AgentCoreExtractionConfig, | ||
| AgentCoreMemoryStoreConfig, | ||
| AgentCoreNamespaceConfig, | ||
| AgentCoreSubtreeStoreConfig, | ||
| CreateAgentCoreMemoryStoresInput, | ||
| ExtractionMode, | ||
| MetadataProvider, | ||
| MetadataValue, | ||
| resolve_namespace, | ||
| slugify_namespace, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "RESERVED_METADATA_PREFIX", | ||
| "AgentCoreEventSender", | ||
| "AgentCoreEventSenderConfig", | ||
| "AgentCoreExactNamespaceStoreConfig", | ||
| "AgentCoreExtractionConfig", | ||
| "AgentCoreMemoryStore", | ||
| "AgentCoreMemoryStoreConfig", | ||
| "AgentCoreNamespaceConfig", | ||
| "AgentCoreSubtreeStoreConfig", | ||
| "CreateAgentCoreMemoryStoresInput", | ||
| "ExtractionMode", | ||
| "MetadataProvider", | ||
| "MetadataValue", | ||
| "assert_writable_topology", | ||
| "create_agentcore_memory_stores", | ||
| "resolve_namespace", | ||
| "slugify_namespace", | ||
| ] |
52 changes: 52 additions & 0 deletions
52
src/bedrock_agentcore/memory/integrations/strands/memorystore/_format.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| """Internal Strands-message formatting helpers.""" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. port lgtm |
||
|
|
||
| from typing import Literal | ||
|
|
||
| from strands.types.content import Message | ||
|
|
||
| AgentCoreRole = Literal["USER", "ASSISTANT"] | ||
|
|
||
|
|
||
| def map_role(message: Message) -> AgentCoreRole: | ||
| """Map a Strands role to the AgentCore conversational-role subset. | ||
|
|
||
| Args: | ||
| message: Strands message to map. | ||
|
|
||
| Returns: | ||
| ``USER`` for a user message, otherwise ``ASSISTANT``. | ||
| """ | ||
| return "USER" if message["role"] == "user" else "ASSISTANT" | ||
|
|
||
|
|
||
| def extract_text(message: Message) -> str: | ||
| """Join non-empty text blocks and ignore all other block kinds. | ||
|
|
||
| Args: | ||
| message: Strands message whose text should be extracted. | ||
|
|
||
| Returns: | ||
| Trimmed blocks joined by newlines. | ||
| """ | ||
| # Drop blank blocks before joining so an empty middle block does not | ||
| # leave a stray blank line in the concatenated event text. | ||
| parts = [] | ||
| for block in message["content"]: | ||
| if "text" not in block: | ||
| continue | ||
| text = block["text"].strip() | ||
| if text: | ||
| parts.append(text) | ||
| return "\n".join(parts) | ||
|
|
||
|
|
||
| def is_user_or_assistant_with_text(message: Message) -> bool: | ||
| """Return whether a user/assistant message contains extractable text. | ||
|
|
||
| Args: | ||
| message: Strands message to inspect. | ||
|
|
||
| Returns: | ||
| ``True`` only for a supported role with non-blank text. | ||
| """ | ||
| return message["role"] in ("user", "assistant") and bool(extract_text(message)) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General high level.
We missed a lot of the existing inline code comments from the typescript package during the port.
Make sure we are bringing over any comments that are relevant to the python version