Skip to content

feat(memory): add AgentCoreMemoryStore Strands integration#588

Open
strandly-the-agent wants to merge 3 commits into
aws:mainfrom
strandly-the-agent:port/agentcore-memory-store-to-python
Open

feat(memory): add AgentCoreMemoryStore Strands integration#588
strandly-the-agent wants to merge 3 commits into
aws:mainfrom
strandly-the-agent:port/agentcore-memory-store-to-python

Conversation

@strandly-the-agent

@strandly-the-agent strandly-the-agent commented Jul 21, 2026

Copy link
Copy Markdown

TL;DR: Ports the TypeScript AgentCoreMemoryStore to Python with the same retrieval, batching, metadata, extraction, and one-writer behavior. Final revision 56dcff8 addresses all current review feedback and a Python overflow edge while preserving source behavior.

Revision after review

  • Contained the new implementation under bedrock_agentcore.memory.integrations.strands.memorystore.
  • Moved the existing SessionManager implementation under strands.memorysessionmanager; existing imports and patch paths remain exact module aliases.
  • Removed runtime Strands version probing and rely on the declared strands-agents>=1.46.0 floor.
  • Replaced ECMAScript emulation with Python-native str.strip() and deterministic json.dumps() behavior, as requested.
  • Preserved TypeScript's Math.min(Math.ceil(...), 100) behavior for overflow-sized finite over-fetch factors without adding new product behavior.
  • Carried over the relevant TypeScript rationale comments.
  • Split the SessionManager and MemoryStore guides without replacement/deprecation claims.
  • Restored TESTING.md to its pre-PR scope and reused MEMORY_PREPOPULATED_ID in the live suite.
  • Corrected the flush() documentation and made the first MemoryStore example standalone.

Source: TypeScript PR #187, merged and inspected on main at c7cb2bca47258108771e87966901d651aee3cef8
Target revision: 56dcff8d498165d2b82781d084b1946d0117e294
Closes #586

Behavior traceability
Source behavior Python target Result
Role mapping, text extraction, unsupported-message filtering memorystore/test_format.py PASS
Ordered turn packing and event chunking memorystore/test_sender.py PASS
Stable sequence/run-scoped idempotency tokens memorystore/test_sender.py PASS
Consecutive metadata grouping, scalar mapping, and validation before I/O memorystore/test_sender.py PASS
Attempt-all event dispatch and aggregate failure handling memorystore/test_sender.py PASS
Exact/subtree retrieval request shape and record mapping memorystore/test_store.py PASS
Score-floor over-fetch/filter/trim, overflow-safe source cap, and result validation memorystore/test_store.py PASS
Writable add_messages and recall-only rejection memorystore/test_store.py PASS
Multi-namespace factory, shared client, extraction mapping, and one-writer topology memorystore/test_factory.py PASS
Namespace placeholder resolution and validation memorystore/test_types.py, test_store.py, test_factory.py PASS
Static MemoryStore/MemoryManager composition and absent optional methods memorystore/test_static_typing.py PASS
Existing SessionManager import and patch-point compatibility after containment memorysessionmanager/test_compatibility_imports.py PASS
Source live behaviors: idempotent writes, extraction modes, exact/subtree recall, namespace isolation, real-agent composition tests_integ/.../memorystore/test_memory_store.py MAPPED; live not run

The 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
uv run pytest tests/ -q
2952 passed, 10 skipped, 4 xpassed, 48 warnings in 82.12s

uv run pytest tests/bedrock_agentcore/memory/integrations/strands -q
386 passed, 2 warnings in 30.73s

uv run pytest --collect-only -q tests_integ/memory/integrations/strands
18 tests collected

uv run ruff check .
All checks passed!

uv run ruff format --check .
305 files already formatted

uv lock --check
Resolved 176 packages

git diff --check
(no output)

uv build
Successfully built dist/bedrock_agentcore-1.18.1.tar.gz
Successfully built dist/bedrock_agentcore-1.18.1-py3-none-any.whl

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
TypeScript source Python target
format.ts strands/memorystore/_format.py
sender.ts strands/memorystore/sender.py
store.ts strands/memorystore/store.py
factory.ts strands/memorystore/factory.py
types.ts strands/memorystore/types.py
index.ts strands/memorystore/__init__.py
MemoryStore guide strands/memorystore/README.md

Decisions:

  1. TypedDict is used for TypeScript object-literal configuration interfaces.
  2. Synchronous boto3 calls remain off the event loop via asyncio.to_thread.
  3. asyncio.gather(..., return_exceptions=True) preserves source attempt-all behavior.
  4. AggregateMemoryError is the Python Strands equivalent of the source aggregate error.
  5. Python-native trimming/JSON serialization replaces ECMAScript helper emulation per maintainer direction.
  6. Existing SessionManager paths alias canonical module objects so existing imports and unittest.mock.patch targets keep working.
  7. No new dependencies were added; the existing boto3/botocore and Strands floors remain.
Sensitive-surface and capability delta
  • Network: the new MemoryStore calls only AgentCore create_event and retrieve_memory_records, matching the TypeScript source.
  • Credentials: boto3's normal credential chain; credentials are not stored.
  • Filesystem/subprocess/deserialization: no new production filesystem or subprocess surface. Static typing tests invoke mypy in a child process.
  • Workflow: adds the contained live MemoryStore tests to the existing memory matrix and reuses MEMORY_PREPOPULATED_ID; no AWS resource creation is added.
  • Dependencies: raises boto3/botocore to 1.43.35 and strands-agents to 1.46.0; no new package.

Open verification

  • Live AWS execution remains outstanding.
  • CI for revision 56dcff8 is subject to the repository's normal security approval path.

@strandly-the-agent
strandly-the-agent requested a review from a team July 21, 2026 20:05
@arielnabavian

Copy link
Copy Markdown

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."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

port lgtm

@@ -0,0 +1,62 @@
"""Internal Strands-message formatting helpers."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

port lgtm


## Native Strands long-term memory stores

`AgentCoreMemoryStore` plugs AgentCore long-term memory directly into Strands' `MemoryManager`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets verify there is no existing MEMORY_STORE_TEST_ID we can re-use from the legacy AgentCoreMemorySessionManager.

Comment thread TESTING.md Outdated
uv run pytest tests_integ/memory/test_memory_client.py -xvs
```

### Strands memory-store integration

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread TESTING.md Outdated

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not put paths like this in any comments or markdown documentation. Reference the class or primitive implementation as needed

Comment thread TESTING.md Outdated
`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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have all these new memorystore related changes under /strands/memorystore so they are more contained

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: ""

Copy link
Copy Markdown

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

return f"{self._memory_id}-{self._actor_id}-{self._run_id}-{first}-{last}"


def _ecmascript_number_string(value: int | float) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in general for this whole port. this is a python package. lets not try to reach super optimized parity with ecmascript.

@arielnabavian

Copy link
Copy Markdown

@strandly-the-agent review this

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.
⚠️ Live AWS was not run; the repository safety gate intentionally stopped the changed workflow.
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 CreateEvent preflight, and the corrected flush() 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},

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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:

@strandly-the-agent strandly-the-agent Jul 22, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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

@strandly-the-agent strandly-the-agent Jul 22, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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:

Suggested change
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.

@arielnabavian

Copy link
Copy Markdown

@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.

Comment on lines +42 to +50
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,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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",

@strandly-the-agent

Copy link
Copy Markdown
Author

Revision pushed at bac5ffee: all current maintainer comments are addressed while keeping the MemoryStore behavior aligned with the TypeScript source.

  • Contained the APIs under strands.memorystore / strands.memorysessionmanager; preserved existing SessionManager imports and patch paths.
  • Removed runtime version probing and ECMAScript-emulation helpers; retained the source retrieval/write semantics without adding pagination or service preflight.
  • Reused MEMORY_PREPOPULATED_ID, restored unrelated TESTING.md content, transferred relevant source rationale comments, and corrected the new docs/examples.
  • Verified: 2952 passed, 10 skipped, 4 xpassed; Ruff and lock checks passed; 18 live tests collect; wheel build passed. Live AWS was not run.
Review disposition

The 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.

@strandly-the-agent

Copy link
Copy Markdown
Author

Final parity follow-up pushed at 56dcff8.

The independent correctness pass found one Python-only overflow edge in over-fetch calculation: a huge finite factor overflowed to inf, where Python math.ceil(inf) raised instead of matching TypeScript's Math.min(Math.ceil(...), 100). The calculation now clamps before integer conversion and has a regression test. This preserves source behavior; it does not add retrieval pagination or service preflight.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port AgentCoreMemoryStore to Python

3 participants