feat(memory): add AgentCoreMemoryStore Strands integration#588
feat(memory): add AgentCoreMemoryStore Strands integration#588strandly-the-agent wants to merge 3 commits into
Conversation
|
Meant to set this as draft, doing some work to validate parity, ignore for now. |
| @@ -0,0 +1,145 @@ | |||
| """Factory helpers for multi-namespace AgentCore memory topologies.""" | |||
| @@ -0,0 +1,62 @@ | |||
| """Internal Strands-message formatting helpers.""" | |||
|
|
||
| ## Native Strands long-term memory stores | ||
|
|
||
| `AgentCoreMemoryStore` plugs AgentCore long-term memory directly into Strands' `MemoryManager`. |
There was a problem hiding this comment.
This is meant to replace AgentCoreMemorySessionManager implementation long term. Lets remove mentions of AgentCoreMemorySessionManager since it long term will be on deprecation path
| MEMORY_KINESIS_ARN: ${{ secrets.MEMORY_KINESIS_ARN }} | ||
| MEMORY_ROLE_ARN: ${{ secrets.MEMORY_ROLE_ARN }} | ||
| MEMORY_PREPOPULATED_ID: ${{ secrets.MEMORY_PREPOPULATED_ID }} | ||
| MEMORY_STORE_TEST_ID: ${{ secrets.MEMORY_STORE_TEST_ID }} |
There was a problem hiding this comment.
Lets verify there is no existing MEMORY_STORE_TEST_ID we can re-use from the legacy AgentCoreMemorySessionManager.
| uv run pytest tests_integ/memory/test_memory_client.py -xvs | ||
| ``` | ||
|
|
||
| ### Strands memory-store integration |
There was a problem hiding this comment.
Same comment from above,
Let's verify there is no existing MEMORY_STORE_TEST_ID we can re-use from the legacy AgentCoreMemorySessionManager.
If this is available.
|
|
||
| Integration tests run via `integration-testing.yml` on PRs to main. The workflow runs runtime, memory (stream delivery only), and evaluation tests in parallel. | ||
| Integration tests run via `integration-testing.yml` on PRs to main. The workflow runs runtime, | ||
| control-plane/client/session-manager/native memory-store memory tests, evaluation, services, policy, |
There was a problem hiding this comment.
Please do not put paths like this in any comments or markdown documentation. Reference the class or primitive implementation as needed
| `MEMORY_STORE_TEST_ID` or fall back to `MEMORY_PREPOPULATED_ID`. | ||
|
|
||
| Stream delivery tests fail in CI unless `MEMORY_KINESIS_ARN` and `MEMORY_ROLE_ARN` secrets are configured on the repo. Other memory integration tests are not yet run in CI due to provisioning times and flaky LLM-dependent assertions — CI support is planned once test stability is addressed. | ||
| Changes to `.github/workflows/` trigger the workflow security gate and require maintainer security |
There was a problem hiding this comment.
Valid to add this for agents to read, but not relevant to this port. We can add this in a separate PR.
| @@ -0,0 +1,211 @@ | |||
| """Public types and namespace helpers for the Strands AgentCore memory store.""" | |||
There was a problem hiding this comment.
Can we have all these new memorystore related changes under /strands/memorystore so they are more contained
There was a problem hiding this comment.
Move AgentCoreMemorySessionManager related changes seperate /strands/ a /strands/memorysessionmanager. Do not mention this is legacy/deprecated yet, will have this in a follow up.
| @@ -1,5 +1,145 @@ | |||
| """Strands integration for Bedrock AgentCore Memory.""" | |||
| """Strands integrations for Bedrock AgentCore Memory.""" | |||
There was a problem hiding this comment.
This runtime check is overkill
we have raised the minor version requirement to "strands-agents>=1.46.0". So client will just upgrade their strands version on the next agentcore release.
| @@ -124,8 +124,8 @@ jobs: | |||
| ignore: "" | |||
There was a problem hiding this comment.
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
| return f"{self._memory_id}-{self._actor_id}-{self._run_id}-{first}-{last}" | ||
|
|
||
|
|
||
| def _ecmascript_number_string(value: int | float) -> str: |
There was a problem hiding this comment.
We are hyper-optimizing with these ecmascript helper functions. Please just rely use json.dumps(metadata, sort_keys=True)
| return "{" + ",".join(entries) + "}" | ||
|
|
||
|
|
||
| def _metadata_scalar_string(key: str, value: object) -> str: |
There was a problem hiding this comment.
this is _metadata_scalar_string is overkill aswell lets maintain parity with a logic like
if value is None or (isinstance(value, float) and not math.isfinite(value)):
raise ValueError(...)
string_value = value if isinstance(value, str) else json.dumps(value)
|
|
||
| from bedrock_agentcore._utils.user_agent import build_user_agent_suffix | ||
|
|
||
| from ._format import _ecmascript_trim |
There was a problem hiding this comment.
in general for this whole port. this is a python package. lets not try to reach super optimized parity with ecmascript.
|
@strandly-the-agent review this |
There was a problem hiding this comment.
🔴 Changes requested: retrieval silently truncates requested counts, and invalid CreateEvent groups can partially commit.
Verified at exact head 8a9543ee: ✅ 380 focused tests · ✅ Ruff lint/format · ✅ build · ✅ diff-check · ✅ cold retrieval, request-bound, flush, and README repros.
Review set: 2 blockers + 2 docs should-fixes. Focused human approval should confirm the stable package-root exports; this repo has no API-label or API-meeting requirement.
Per-pass reconciliation
- Triage: Accepted and routed; treated as routing only because service-contract interactions were not tested.
- Context: Confirmed exact head, public call graph, Strands 1.46 and boto3 1.43.35 contracts, TypeScript parity, maintainer threads, and review state.
- Correctness: Retained retrieval, complete
CreateEventpreflight, and the correctedflush()contract. - API/DevX: Added no thread; focused human approval of the package-root surface remains appropriate, without a label or meeting requirement.
- Adversarial: Cold-reproduced retrieval truncation/caps, invalid-group partial commits, swallowed-and-retried flush failures, and the README
NameError. - Test-quality: Confirmed the retained behavior gaps; duplicate test-only observations were not posted separately.
- LLM-context: No additional finding.
- Docs: Retained the
flush()correction and standalone README example; lower-priority overlap was suppressed. - Aggregation: Final set is 2 reproduced blockers and 2 deterministic docs should-fixes; speculative, duplicate, and maintainer-owned items were culled.
Submitted as COMMENTED because GitHub does not allow an author to request changes on their own PR; the substantive verdict remains changes requested.
| top_k = min(math.ceil(want * self._over_fetch_factor), MAX_TOPK) | ||
| kwargs: dict[str, Any] = { | ||
| "memoryId": self._memory_id, | ||
| "searchCriteria": {"searchQuery": query, "topK": top_k}, |
There was a problem hiding this comment.
🔴 Retrieval silently truncates the requested count. max_search_results=50 sets topK=50, but this omits top-level maxResults (service default: 20), and the response path below ignores nextToken. Two cold local runs returned 20 entries after one call. Values above 100 also emit an invalid topK because AgentCore caps it at 100.
Please set maxResults, follow pagination until the requested count/no token, and reject or otherwise define behavior for requests above the service cap.
| Raises: | ||
| ValueError: If ``max_turns_per_event`` is not a positive integer. | ||
| """ | ||
| if type(max_turns_per_event) is not int or max_turns_per_event < 1: |
There was a problem hiding this comment.
🔴 Preflight every CreateEvent group before dispatching any of them. Cold repros passed a 101-item payload, 100,001-character text, 16 metadata entries, a 129-character key, and a 257-character value into the client; botocore did not reject them locally. AgentCore limits these to 100 payload items, 100,000 text characters, 15 metadata entries, 1–128-character patterned keys, and 0–256-character patterned values.
Because _send_all dispatches sibling groups concurrently, an invalid group can fail only after another group has committed. Please fully materialize and validate every group before gather(), cap max_turns_per_event at 100, and update the tests to cover exact boundaries plus zero client calls when a later group is invalid. The README’s “any positive integer” claim must change with that cap.
| Strands' default trigger. Pass an extraction config with an `IntervalTrigger` (or another Strands | ||
| trigger) to tune cadence. Cadence only batches across invocations when the same `MemoryManager` is | ||
| reused and the application does not flush after every turn. | ||
| 3. **`flush()` is the durability boundary, not a server-extraction barrier.** It waits for in-flight |
There was a problem hiding this comment.
🟡 Correct the flush() contract throughout this section. In Strands 1.46, the extraction coordinator catches sender failures, rolls back its high-water mark, retains the batch for retry, logs the failure, and returns. In the cold repro, the first flush() returned after a failed write and the second retried successfully, so this is neither an exception-propagation boundary nor a persistence acknowledgment.
At this single-line anchor:
| 3. **`flush()` is the durability boundary, not a server-extraction barrier.** It waits for in-flight | |
| 3. **`flush()` lets pending write attempts settle; it does not acknowledge durability or server-side extraction.** It waits for in-flight |
Please make the matching correction to lines 134–142 and 160–163: synchronous agent(...) flushes automatically; async callers should flush at a lifecycle/shutdown boundary; failed batches remain available for retry, but failures are logged and swallowed rather than propagated. Applications must monitor logs or telemetry instead of treating flush() as proof of persistence.
|
@strandly-the-agent read all the comments on this pr. Read the comments thoroughly and makes sure we are filling the requirements of the given task, (port typescript AgentCoreMemoryStore to python AgentCoreMemoryStore). Make sure not to add any logic changes outside of whats required for 1-1 parity. Make sure the code comments are all address, make a revision to the PR once you are done. |
| from strands import Agent | ||
| from strands.memory import MemoryManager | ||
|
|
||
| from bedrock_agentcore.memory.integrations.strands import AgentCoreMemoryStore | ||
|
|
||
| store = AgentCoreMemoryStore( | ||
| memory_id=MEM_ID, | ||
| actor_id=ACTOR_ID, | ||
| session_id=SESSION_ID, |
There was a problem hiding this comment.
🟡 Make the first example runnable as a standalone snippet. As written, it raises NameError because MEM_ID, ACTOR_ID, and SESSION_ID have not been defined. Definitions in later, unrelated examples do not make this snippet executable.
| from strands import Agent | |
| from strands.memory import MemoryManager | |
| from bedrock_agentcore.memory.integrations.strands import AgentCoreMemoryStore | |
| store = AgentCoreMemoryStore( | |
| memory_id=MEM_ID, | |
| actor_id=ACTOR_ID, | |
| session_id=SESSION_ID, | |
| import os | |
| from strands import Agent | |
| from strands.memory import MemoryManager | |
| from bedrock_agentcore.memory.integrations.strands import AgentCoreMemoryStore | |
| store = AgentCoreMemoryStore( | |
| memory_id=os.environ["AGENTCORE_MEMORY_ID"], | |
| actor_id="demo-user", | |
| session_id="demo-session", |
|
Revision pushed at
Review dispositionThe two earlier Strandly implementation blockers requested behavior beyond the TypeScript source. Per the explicit 1:1-parity direction, this revision intentionally does not implement them. The two documentation findings are fixed. Full traceability and validation evidence are in the updated PR description. |
|
Final parity follow-up pushed at The independent correctness pass found one Python-only overflow edge in over-fetch calculation: a huge finite factor overflowed to Current focused validation: 386 passed; Ruff, format, lock, and diff checks pass. The PR description now points to the final head and updated traceability evidence. Live AWS remains unrun. |
Revision after review
bedrock_agentcore.memory.integrations.strands.memorystore.strands.memorysessionmanager; existing imports and patch paths remain exact module aliases.strands-agents>=1.46.0floor.str.strip()and deterministicjson.dumps()behavior, as requested.Math.min(Math.ceil(...), 100)behavior for overflow-sized finite over-fetch factors without adding new product behavior.TESTING.mdto its pre-PR scope and reusedMEMORY_PREPOPULATED_IDin the live suite.flush()documentation and made the first MemoryStore example standalone.Source: TypeScript PR #187, merged and inspected on
mainatc7cb2bca47258108771e87966901d651aee3cef8Target revision:
56dcff8d498165d2b82781d084b1946d0117e294Closes #586
Behavior traceability
memorystore/test_format.pymemorystore/test_sender.pymemorystore/test_sender.pymemorystore/test_sender.pymemorystore/test_sender.pymemorystore/test_store.pymemorystore/test_store.pyadd_messagesand recall-only rejectionmemorystore/test_store.pymemorystore/test_factory.pymemorystore/test_types.py,test_store.py,test_factory.pyMemoryStore/MemoryManagercomposition and absent optional methodsmemorystore/test_static_typing.pymemorysessionmanager/test_compatibility_imports.pytests_integ/.../memorystore/test_memory_store.pyThe TypeScript source intentionally performs one retrieval request without pagination and dispatches event groups without Python-only service-bound preflight. This revision preserves those behaviors per the strict parity requirement.
Validation evidence
The wheel contains both canonical packages and all pre-existing compatibility modules. Live AWS tests were not run because no live
MEMORY_PREPOPULATED_ID/approved execution was available. The changed workflow remains subject to the repository safety gate.Structural map and decisions
format.tsstrands/memorystore/_format.pysender.tsstrands/memorystore/sender.pystore.tsstrands/memorystore/store.pyfactory.tsstrands/memorystore/factory.pytypes.tsstrands/memorystore/types.pyindex.tsstrands/memorystore/__init__.pystrands/memorystore/README.mdDecisions:
TypedDictis used for TypeScript object-literal configuration interfaces.asyncio.to_thread.asyncio.gather(..., return_exceptions=True)preserves source attempt-all behavior.AggregateMemoryErroris the Python Strands equivalent of the source aggregate error.unittest.mock.patchtargets keep working.Sensitive-surface and capability delta
create_eventandretrieve_memory_records, matching the TypeScript source.MEMORY_PREPOPULATED_ID; no AWS resource creation is added.1.43.35andstrands-agentsto1.46.0; no new package.Open verification
56dcff8is subject to the repository's normal security approval path.