Skip to content

feat(memory): add AgentCoreMemoryStore Strands integration - #588

Open
strandly-the-agent wants to merge 2 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 2 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 Strands integration to Python as a contained package. Revision 7bc3595 rebases onto current main, implements jariy17's three code asks, and drops the integration-testing workflow change entirely — the PR now touches no existing file except pyproject.toml/uv.lock.

Review responses in this revision

Ask Response
Fold the message helpers into AgentCoreEventSender (r3707303752) Done. Usage check showed all three were sender-exclusive, so _format.py is gone and they are _map_role / _extract_text / _is_user_or_assistant_with_text on the class.
Use the MemoryClient we already vend (r3708398179) Now supported: client=MemoryClient(...) is accepted by the store, factory and sender, and its vended gmdp_client is used. MemoryClient's own create_event is a (text, role) convenience API with no clientToken, so the raw data-plane surface is still what the store calls.
_assert_writable_topology (r3708398590) Renamed and removed from the package __all__; it is an internal topology guard.
Why raise the integration-test timeout to 30m? (r3660049235) Hunk dropped — .github/workflows/integration-testing.yml is byte-identical to main. Wiring the live suite into CI is a maintainer-owned change; details on the thread.

Source: TypeScript PR #187 at c7cb2bca47258108771e87966901d651aee3cef8 · Target: 7bc3595 · Closes #586

Scope: what this PR changes
pyproject.toml                                    (strands-agents floor >= 1.46.0, boto3/botocore >= 1.43.35)
uv.lock
src/.../strands/memorystore/{__init__,factory,sender,store,types}.py, README.md   (new)
tests/.../strands/memorystore/**                                                   (new)
tests_integ/memory/integrations/test_memory_store.py                               (new)

No existing module, test, doc, or workflow is modified. griffe (the Breaking Change Check's own call) reports 0 breaking changes against main, and a runtime probe of the pre-existing Strands modules (dir(), __all__, logger.name, owning module per public symbol) is identical to a main worktree — import paths, patch targets and logger names are untouched.

Behavior traceability matrix (TypeScript test → Python test)

Source paths are relative to src/memory/integrations/strands/__tests__/; target paths to tests/bedrock_agentcore/memory/integrations/strands/memorystore/.

Source test (file:line) Behavior Target test (file:line)
format.test.ts:13,16 mapRole user/assistant → USER/ASSISTANT test_message_formatting.py:19
format.test.ts:22,25,28 join text blocks, ignore non-text, empty when none test_message_formatting.py:24, :54
format.test.ts:31 trim surrounding whitespace test_message_formatting.py:40
format.test.ts:37,40,43,46 only user/assistant with extractable text test_message_formatting.py:68
sender.test.ts:57 whole batch → one createEvent, role-tagged, ordered test_sender.py:66
sender.test.ts:70 split at maxTurnsPerEvent test_sender.py:80
sender.test.ts:79,238 skip tool-only/empty; no-op batch test_sender.py:93
sender.test.ts:86,136 omit clientToken without complete sequence numbers test_sender.py:114
sender.test.ts:92,105 deterministic per-chunk seq-range token, stable across a re-fire test_sender.py:126
sender.test.ts:111,122 token anchored on run id; default fresh UUID test_sender.py:142, :150
sender.test.ts:142,153 new event when metadata changes; consecutive-only grouping test_sender.py:167
sender.test.ts:166,198 map constant metadata to stringValue; omit empty bag test_sender.py:188, :421
sender.test.ts:179 charset violation raises before any createEvent test_sender.py:212, :230, :238
sender.test.ts:188 nullish/non-finite metadata rejected test_sender.py:247, :358
sender.test.ts:205,213,222 attempt every event, then aggregate error folding the first reason test_sender.py:259, :281
sender.test.ts:244 reject non-positive maxTurnsPerEvent test_sender.py:291
sender.test.ts:248,255 extractionMode passthrough only when configured test_sender.py:303
store.test.ts:67,401 resolve {actorId}/{sessionId}, query exact namespace test_store.py:58
store.test.ts:75 subtree mode uses namespacePath test_store.py:67
store.test.ts:83 MemoryRecordSummaryMemoryEntry mapping test_store.py:81
store.test.ts:105,137 topK == want without a score floor test_store.py:107, :395
store.test.ts:112 over-fetch, filter by floor, trim to want test_store.py:114
store.test.ts:128,144,155 custom factor, ceil to int, cap at 100 test_store.py:134, :405
store.test.ts:172 unscored records treated as 0 under a floor test_store.py:155
store.test.ts:179,195,201 non-text → empty, [] on empty, errors propagate test_store.py:162, :170, :175
store.test.ts:211,225,237 writable store forwards messages + sequence numbers, batched test_store.py:183
store.test.ts:251 non-writable store rejects addMessages test_store.py:206
store.test.ts:259,268,277,309 extraction only on the writer; warn only when dropped test_store.py:212, :225
store.test.ts:285,299 self-name from namespace; slug fallback for a blank name test_store.py:233, :253
store.test.ts:292 writable defaults to false test_store.py:268
store.test.ts:317,341 stands alone with flat identity; identity validation test_store.py:280, :302
store.test.ts:369,373,380 empty namespace / unresolved placeholder (exact + subtree) test_store.py:318, :339
store.test.ts:395 $-sequences in {actorId} inserted verbatim test_store.py:345, :382
factory.test.ts:29,34,41 one store per namespace, one default writer, extraction on writer test_factory.py:42
factory.test.ts:47,54 custom cadence/filter reach the writer test_factory.py:53
factory.test.ts:61,76 explicit writable flag / opt-out selection test_factory.py:62, :79
factory.test.ts:91,105 all opted out, or two writers → error test_factory.py:96, :110
factory.test.ts:118,126,131 recall-only modes; extraction: true passthrough test_factory.py:124, :134
factory.test.ts:140,152 derived/explicit names, default fallback test_factory.py:140
factory.test.ts:162,166,180 namespace validation test_factory.py:166, :173, :191
factory.test.ts:188 actor/session bound per call test_factory.py:249
factory.test.ts:208,212,216,222,226 writable-topology assertions test_factory.py:219, :225, :233
tests_integ/memory.test.ts (live) idempotent batched writes, extraction modes, exact/subtree recall, namespace isolation, real-agent round trip tests_integ/memory/integrations/test_memory_store.py:66, :113, :161, :177, :212, :254not executed (needs live AWS)

Deliberate divergences from the source, both requested in review: the topK clamp on every retrieval path (test_store.py:405, :421, :428, :439) and Python-native str.strip()/json.dumps metadata handling instead of ECMAScript emulation (test_message_formatting.py:40, test_sender.py:381, :406).

Python-only tests cover Python-runtime concerns rather than new product behavior: MemoryClient acceptance (test_store.py:458, test_factory.py:262), blocking boto3 call kept off the event loop (test_sender.py:314), cancellation settlement (test_sender.py:430, :452), region/session resolution (test_store.py:475:548), TypedDict runtime metadata (test_types.py:18), package exports (test_package_exports.py), and static typing fixtures (test_static_typing.py, incl. a MemoryClient case).

Validation at 7bc3595
pytest tests/ -q --ignore=.../third_party/deepeval   → 3028 passed, 10 skipped, 4 xpassed, 0 failed
pytest tests/bedrock_agentcore/memory/integrations/strands -q → 388 passed
ruff check .                                        → All checks passed!
ruff format --check .                               → 312 files already formatted
mypy src/.../strands/memorystore                    → Success: no issues found in 5 source files
griffe find_breaking_changes vs origin/main         → 0
git diff --check                                    → clean

Live AWS was not run (the suite needs MEMORY_PREPOPULATED_ID plus Bedrock model access). Two independent review passes were run over the review-response commit; both findings (widened client types not applied to the factory/TypedDicts, two stale docstrings) are fixed here.

Open items
  • Retrieval pagination (maxResults/nextToken) stays a fast follow, per arielnabavian on the store.py thread; a request above 100 records returns the first 100.
  • Full CreateEvent request preflight (payload/text/metadata service bounds) also matches the source and is in the same fast-follow bucket.
  • CI wiring for the live suite is not in this PR (see the workflow thread): the memory matrix group would need tests_integ/memory/integrations/test_memory_store.py, pytest-asyncio, and a timeout that accommodates eventual-consistency polling.

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

path: tests_integ/runtime
timeout: 10
extra-deps: ""
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.

Superseded — reconciled at 8f2fac4. This review was written against 8a9543ee; the verdict below no longer reflects the current state.

  • The two documentation findings (flush() semantics, non-runnable first example) are fixed in the contained memorystore/README.md; their original threads pointed at the old root README, which is now byte-identical to main, so I deleted those two stale comments rather than leave them dangling.
  • The two service-contract findings (retrieval pagination/maxResults, full CreateEvent request preflight) are intentionally not implemented: they are behavior the TypeScript source does not have, and arielnabavian confirmed on the pagination thread that this matches TypeScript and belongs in a fast follow. Those two threads stay open as that record — no further nagging from me.
  • Nothing here is a blocker on the current head. Verification evidence for 8f2fac4 is in the PR description.

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.

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 the same implementation in typescript. I suggest we fix this as a fast follow

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.

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

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

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.51884% with 131 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@a4bc13f). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...ns/strands/memorysessionmanager/session_manager.py 92.18% 19 Missing and 20 partials ⚠️
.../strands/memorysessionmanager/converters/openai.py 68.80% 19 Missing and 15 partials ⚠️
...e/memory/integrations/strands/memorystore/store.py 86.13% 7 Missing and 7 partials ⚠️
...ore/memory/integrations/strands/session_manager.py 28.57% 10 Missing ⚠️
...memory/integrations/strands/converters/protocol.py 0.00% 8 Missing ⚠️
...e/memory/integrations/strands/converters/openai.py 0.00% 6 Missing ⚠️
...ck_agentcore/memory/integrations/strands/config.py 44.44% 4 Missing and 1 partial ⚠️
...e/memory/integrations/strands/bedrock_converter.py 57.14% 2 Missing and 1 partial ⚠️
...memory/integrations/strands/converters/__init__.py 72.72% 2 Missing and 1 partial ⚠️
.../strands/memorysessionmanager/bedrock_converter.py 94.23% 0 Missing and 3 partials ⚠️
... and 4 more
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #588   +/-   ##
=======================================
  Coverage        ?   89.15%           
=======================================
  Files           ?      116           
  Lines           ?     9531           
  Branches        ?     1428           
=======================================
  Hits            ?     8497           
  Misses          ?      677           
  Partials        ?      357           
Flag Coverage Δ
unittests 89.15% <88.51%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@arielnabavian

arielnabavian commented Jul 27, 2026

Copy link
Copy Markdown

For sanity, ran a manual audit as well full_suite_interval_results.xlsx

@arielnabavian

Copy link
Copy Markdown

@arielnabavian

Copy link
Copy Markdown

@strandly-the-agent make a revision.

Make sure we are not messing up any imports for existing clients. specifically these 1s
Run actions/github-script@v8
RequestError [HttpError]: Resource not accessible by integration
at /home/runner/work/_actions/actions/github-script/v8/dist/index.js:9537:21
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async eval (eval at callAsyncFunction (/home/runner/work/_actions/actions/github-script/v8/dist/index.js:36187:16), :27:3)
at async main (/home/runner/work/_actions/actions/github-script/v8/dist/index.js:36285:20) {
status: 403,
response: {
url: 'https://api.github.com/repos/aws/bedrock-agentcore-sdk-python/issues/588/comments',
status: 403,
headers: {
'access-control-allow-origin': '*',
'access-control-expose-headers': 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset, Warning',
'content-encoding': 'gzip',
'content-security-policy': "default-src 'none'",
'content-type': 'application/json; charset=utf-8',
date: 'Thu, 23 Jul 2026 21:25:52 GMT',
'referrer-policy': 'origin-when-cross-origin, strict-origin-when-cross-origin',
server: 'github.com',
'strict-transport-security': 'max-age=31536000; includeSubdomains; preload',
'transfer-encoding': 'chunked',
vary: 'Accept-Encoding, Accept, X-Requested-With',
'x-accepted-github-permissions': 'issues=write; pull_requests=write',
'x-content-type-options': 'nosniff',
'x-frame-options': 'deny',
'x-github-api-version-selected': '2022-11-28',
'x-github-media-type': 'github.v3; format=json',
'x-github-request-id': '0440:130C5F:E9623B:ED2DED:6A6286E0',
'x-ratelimit-limit': '15000',
'x-ratelimit-remaining': '14992',
'x-ratelimit-reset': '1784843964',
'x-ratelimit-resource': 'core',
'x-ratelimit-used': '8',
'x-xss-protection': '0'
},
data: {
message: 'Resource not accessible by integration',
documentation_url: 'https://docs.github.com/rest/issues/comments#create-an-issue-comment',
status: '403'
}
},
request: {
method: 'POST',
url: 'https://api.github.com/repos/aws/bedrock-agentcore-sdk-python/issues/588/comments',
headers: {
accept: 'application/vnd.github.v3+json',
'user-agent': 'actions/github-script octokit-core.js/5.0.1 Node.js/24',
authorization: 'bearer [REDACTED]',
'content-type': 'application/json; charset=utf-8'
},
body: '{"body":"\n## ⚠️ Breaking Change Warning\n\nFound 3 potential breaking change(s) in this PR:\n\n\u001b[1msrc/bedrock_agentcore/memory/integrations/strands/session_manager.py\u001b[0m:0: logger: \u001b[33mPublic object was removed\u001b[39m\n\u001b[1msrc/bedrock_agentcore/memory/integrations/strands/bedrock_converter.py\u001b[0m:0: logger: \u001b[33mPublic object was removed\u001b[39m\n\u001b[1msrc/bedrock_agentcore/memory/integrations/strands/converters/openai.py\u001b[0m:0: logger: \u001b[33mPublic object was removed\u001b[39m\n\n---\n> Note: This is an automated static analysis check. Some flagged changes may be intentional.\n> Please confirm each item is expected and, if so, add a migration note to CHANGELOG.md."}',
request: {
agent: [Agent],
fetch: [Function: proxyFetch],
hook: [Function: bound bound register]
}
}
}
Error: Unhandled error: HttpError: Resource not accessible by integration

I also recommended moving our converters back under strands folder rather than memorysessionmanager folders since these are more reusable.

Also please investigate this bug we found in a bug bash:

  1. max_search_results > 100 throws a raw service error — the clamp only runs on the min_score path
    Category: recall · Severity: medium — plausible input, opaque failure, inconsistent with sibling path

store.py:208-213 computes top_k:

top_k = want
if self._min_score is not None:
over_fetch = want * self._over_fetch_factor
top_k = MAX_TOPK if over_fetch >= MAX_TOPK else math.ceil(over_fetch)
MAX_TOPK (100, the real service cap) is applied only inside the min_score branch. With no min_score, want goes to the wire verbatim. Construction validates max_search_results >= 1 but never <= 100.

Verified against the live service:

config topK sent result
max_search_results=100, no min_score 100 OK
max_search_results=101, no min_score 101 ValidationException
max_search_results=200, no min_score 200 ValidationException
search(opts={"max_search_results": 200}) 200 ValidationException
max_search_results=200, min_score=0.1 100 OK — clamped
ValidationException: An error occurred (ValidationException) when calling the
RetrieveMemoryRecords operation: 1 validation error detected: Value at
'searchCriteria.topK' failed to satisfy constraint: Member must have value less
than or equal to 100
Adding a min_score makes a previously-broken call start working, which is a confusing signal. Everything else in this port validates eagerly with an AgentCoreMemoryStore:-prefixed message; this one path defers to a raw botocore error.

Fix: hoist the clamp out of the if — top_k = min(top_k, MAX_TOPK) — or reject max_search_results > MAX_TOPK at construction and in search.

Revise the CR.

@strandly-the-agent

strandly-the-agent commented Jul 30, 2026

Copy link
Copy Markdown
Author

Revision pushed at 8c06a2c. All three items from your comment are handled.

1. Imports for existing clients. The griffe step in Detect Breaking Changes exits 0 — the job died on its actions/github-script step (403 Resource not accessible by integration; GITHUB_TOKEN is read-only for fork PRs), so the report you pasted never made it onto the PR. It was right: 3 × logger: Public object was removed, caused by moving the SessionManager into strands/memorysessionmanager/ behind re-export shims. That move also silently renamed the module loggers (...strands.session_manager...memorysessionmanager.session_manager), which would break customer logging config keyed on those names. Reverted: session_manager.py, config.py, bedrock_converter.py, converters/* and the package root are byte-identical to main. griffe now reports 0 breaking changes, and a runtime probe of every pre-existing module (dir(), __all__, logger.name, owning module per symbol) matches a main worktree exactly.

2. Converters back under strands/. Done — strands/converters/{__init__,openai,protocol}.py are byte-identical to main again, so the reusable converters stay where they were and nothing under memorysessionmanager/ exists in this PR.

3. Bug-bash defect: max_search_results > 100. Fixed — MAX_TOPK is now applied on every retrieval path instead of only inside the min_score branch, and a one-time warning reports the reduced cap so it is not silent. Your matrix, reproduced offline at 8c06a2c:

Config topK before topK now
max_search_results=100, no min_score 100 100
max_search_results=101, no min_score 101 → ValidationException 100
max_search_results=200, no min_score 200 → ValidationException 100
search(options={"max_search_results": 200}) 200 → ValidationException 100
max_search_results=200, min_score=0.1 100 100

This is a deliberate divergence from the TypeScript source (which clamps only the over-fetch path) — flagged as such in the PR description so it does not read as a parity slip. I went with hoisting the clamp rather than rejecting >100 eagerly, because rejecting would also break max_search_results=200, min_score=0.1, which works today; one line to flip if you prefer the hard error. The tests that asserted topK == 101 are gone, replaced by 100/101/200 coverage with and without a score floor, the per-call search_memory option, and warn-once behaviour.

Evidence & open items
  • At 8c06a2c: 386 passed focused, 3024 passed full suite (the 2 remaining failures and the deepeval collection error reproduce unchanged on main with the same venv). ruff, ruff format, mypy on the new package, griffe (0), and git diff --check clean; 18 live tests collect (live AWS not run).
  • Branch also merges current main — it was CONFLICTING, now MERGEABLE; pyproject/uv.lock keep main's version and evals pins plus the strands-agents>=1.46.0 floor.
  • Only pre-existing file still modified: two TestAsyncMode cases moved to the public HookRegistry.get_callbacks_for API — required by the 1.46 floor; they fail on main too under 1.46.
  • Pagination stays the fast follow you called on the store.py thread (a request for >100 records returns the first 100). The CreateEvent request-bound preflight is in the same bucket — happy to pull either into this PR.
  • AgentCoreMemorySessionManager containment is the one item I did not carry over, since it is what broke the import surface; ready to open it as a standalone PR.
  • safety-gate stays red by design: this PR still touches .github/workflows/integration-testing.yml to add the new live test to the memory matrix and install pytest-asyncio. Happy to drop that hunk if you would rather wire CI separately.

@github-actions github-actions Bot added size/xl PR size: XL and removed size/xl PR size: XL labels Jul 30, 2026
path: tests_integ/memory/test_controlplane.py tests_integ/memory/test_memory_client.py tests_integ/memory/integrations/test_session_manager.py
timeout: 15
path: tests_integ/memory/test_controlplane.py tests_integ/memory/test_memory_client.py tests_integ/memory/integrations/strands/memorysessionmanager/test_session_manager.py tests_integ/memory/integrations/strands/memorystore/test_memory_store.py
timeout: 30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we increasing this 30 minutes now?

AgentCoreRole = Literal["USER", "ASSISTANT"]


def map_role(message: Message) -> AgentCoreRole:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems like these functions can be part of the AgentCoreSender class as private function _map_roles(). Let's shift these functions into AgentCoreSender if its only used by that class.

RESERVED_METADATA_PREFIX = "_"


class AgentCoreDataPlaneClient(Protocol):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cant we use the MemoryClient we already vend in this repo?

)


def assert_writable_topology(stores: Sequence[MemoryStore], expect_extraction: bool = False) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_assert_writable_topology

@arielnabavian

Copy link
Copy Markdown

@strandly-the-agent address these comments and revise the pr.

Port the TypeScript AgentCoreMemoryStore to Python as a contained package under
memory/integrations/strands/memorystore: exact/subtree recall, role-preserving
batched writes, deterministic idempotency tokens, one-writer extraction topology,
and typed public configuration.

Existing Strands integration modules are untouched, so client import paths, patch
targets and logger names are unchanged (griffe reports no breaking changes).

Review follow-ups included:
- topK is clamped on every retrieval path (a plausible max_search_results above the
  service limit previously failed as a raw ValidationException), with a warn-once log.
- The sender-exclusive message helpers are private methods of AgentCoreEventSender.
- The SDK's own MemoryClient is accepted wherever a data-plane client is, by
  unwrapping the client it already vends.
- The internal topology guard is named _assert_writable_topology.
- The integration-testing workflow is left untouched; wiring the live suite into CI
  is deferred to a maintainer-owned change.
@strandly-the-agent
strandly-the-agent force-pushed the port/agentcore-memory-store-to-python branch from 8c06a2c to ddd0ba5 Compare August 4, 2026 00:23
@github-actions github-actions Bot added size/xl PR size: XL and removed size/xl PR size: XL labels Aug 4, 2026
Drop the memory-group path/timeout and pytest-asyncio hunks; wiring the new live
suite into CI is a maintainer-owned change (review thread r3660049235).
@arielnabavian

Copy link
Copy Markdown

@strandly-the-agent address these comments from
jariy17 and revise the pr

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

Labels

size/xl PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port AgentCoreMemoryStore to Python

5 participants