Skip to content

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

Open
strandly-the-agent wants to merge 6 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 6 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. Revision 8c06a2c merges current main, removes every change to the pre-existing Strands modules (griffe: 0 breaking changes), and fixes the bug-bash topK defect so no plausible max_search_results reaches the service as a raw ValidationException.

What changed in this revision

  • Existing client imports are untouched. session_manager.py, config.py, bedrock_converter.py, converters/* and the package root are byte-identical to main. The earlier relocation into strands/memorysessionmanager/ is reverted: it changed logger names (...strands.session_manager...memorysessionmanager.session_manager), dropped module globals from the static API surface, and relied on a sys.modules alias. The converters therefore live under strands/converters/ again, as requested.
  • Bug-bash fix (store.py). MAX_TOPK is now applied on every retrieval path, not only the min_score branch, and a one-time logger.warning reports the reduced cap so it is not silent. This is a deliberate, maintainer-requested divergence from the TypeScript source, which clamps only the over-fetch path.
  • The new code is contained under bedrock_agentcore.memory.integrations.strands.memorystore and is purely additive.
  • Merged main (was conflicting): pyproject.toml/uv.lock keep main's version and evals pins plus this PR's strands-agents>=1.46.0 floor.
  • Only pre-existing file still modified: two TestAsyncMode cases on the public HookRegistry.get_callbacks_for API — required by the 1.46 floor (they fail on main too under 1.46).

Source: TypeScript PR #187 at c7cb2bca47258108771e87966901d651aee3cef8 · Target: 8c06a2c · Closes #586

Bug-bash matrix: topK on the wire (was → now)

Reproduced offline against a mock data-plane client at 8c06a2c (src/.../memorystore/store.py:210-231):

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

A cap above the limit now logs once per store: AgentCoreMemoryStore <name>: max_search_results=200 exceeds AgentCore's topK limit of 100; searches return at most 100 records per request.

Tests: test_store.py:395 covers 100/101/200 with and without a score floor, :421 covers the per-call search_memory option, :428/:441 cover warn-once and no-warning-at-100. The previous test_result_cap_above_100_is_allowed_without_score_floor, which asserted topK == 101, is gone.

Still deferred to the fast follow you called on the store.py thread: pagination (maxResults/nextToken), so a request for more than 100 records returns the first 100.

Import-compatibility evidence (the failing Breaking Change check)

griffe==1.7.3, the same call the workflow makes, base origin/main vs this branch:

griffe.find_breaking_changes(load_git("bedrock_agentcore", ref="origin/main"), load("bedrock_agentcore"))
→ BREAKAGES: 0

Public member sets of every pre-existing module are unchanged (only addition is the new memorystore submodule):

OK  memory.integrations.strands                      missing=[] added=['memorystore']
OK  memory.integrations.strands.session_manager      missing=[] added=[]
OK  memory.integrations.strands.config               missing=[] added=[]
OK  memory.integrations.strands.bedrock_converter    missing=[] added=[]
OK  memory.integrations.strands.converters           missing=[] added=[]
OK  memory.integrations.strands.converters.openai    missing=[] added=[]
OK  memory.integrations.strands.converters.protocol  missing=[] added=[]

Runtime probe (import each module from a main worktree and from this branch; compare dir(), __all__, logger.name, and the owning module of every public symbol): identical, including the three logger names.

At the earlier head the same check reported 7 breakages: 3 × logger: Public object was removed (from the relocation) and 4 caused by the branch being behind main.

The red Detect Breaking Changes run is the workflow's actions/github-script step getting 403 Resource not accessible by integrationGITHUB_TOKEN is read-only for fork PRs, so the informational comment cannot be posted. The griffe step itself exits 0; nothing in this PR can fix that permission (it needs pull_request_target or a PAT in the workflow).

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_format.py:19
format.test.ts:22,25,28 join text blocks, ignore non-text, empty when none test_format.py:24, :54
format.test.ts:31 trim surrounding whitespace test_format.py:40
format.test.ts:37,40,43,46 only user/assistant with extractable text test_format.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
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
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:247
factory.test.ts:208,212,216,222,226 writable-topology assertions test_factory.py:219, :225, :231
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:67, :114, :162, :178, :213, :255not executed (needs live AWS)

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

Other Python-only tests cover Python-runtime concerns rather than new product behavior: 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:441:514), TypedDict runtime metadata (test_types.py:18), package exports (test_package_exports.py), static typing fixtures (test_static_typing.py), and parity guards proving no Python-only request preflight was added (test_sender.py:332, :337, :345).

Validation at 8c06a2c
pytest tests/ -q --ignore=.../third_party/deepeval
  3024 passed, 10 skipped, 4 xpassed, 2 failed

pytest tests/bedrock_agentcore/memory/integrations/strands -q
  386 passed

ruff check .            → All checks passed!
ruff format --check .   → 313 files already formatted
mypy src/.../strands/memorystore → Success: no issues found in 6 source files
griffe find_breaking_changes vs origin/main → 0
git diff --check        → clean
pytest --collect-only tests_integ/memory/integrations → 18 tests collected

The 2 failures and the deepeval collection error are pre-existing on main — reproduced in a main worktree with the same interpreter/venv: evaluation/custom_code_based_evaluators/test_models.py::TestEvaluatorOutput::test_label_required_without_error_code, evaluation/integrations/strands_agents_evals/test_end_to_end.py::...::test_evaluation_with_empty_trajectory, and third_party/deepeval needing the optional deepeval package. mypy on session_manager.py reports the same pre-existing errors as main.

Live AWS was not run. The safety-gate failure is the repository's intended gate for any PR touching .github/workflows/** (this PR adds the new live test to the memory matrix and installs pytest-asyncio); a maintainer must trigger integration tests manually.

Open items / decisions
  • Retrieval pagination stays a fast follow per your call on the store.py thread; the topK-cap half of that thread is fixed here.
  • Full CreateEvent request preflight (payload/text/metadata service bounds) also stays as the source behaves — same fast-follow bucket. Say the word if you want it in this PR.
  • AgentCoreMemorySessionManager containment under strands/memorysessionmanager: not in this PR — it is what broke the import surface. Ready to open it as a standalone PR.
  • Documentation for the new store lives in the contained memorystore/README.md (corrected flush() semantics, standalone first example); no existing doc was touched.

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

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 added 2 commits July 30, 2026 19:57
Revert the AgentCoreMemorySessionManager relocation: session_manager, config,
bedrock_converter and converters are byte-identical to main again, so existing
import paths, patch targets, logger names and static API surface are unchanged.

The new port stays contained under integrations/strands/memorystore. The only
change to pre-existing files is a two-test adaptation to the public HookRegistry
API required by the strands-agents>=1.46 floor.
@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.

max_search_results above AgentCore's topK limit of 100 reached the wire verbatim
and failed as a raw ValidationException, while the same value with min_score set
succeeded because the clamp lived inside the score-filter branch. Clamp on every
path and warn once so the reduced cap is not silent.

Deliberate divergence from the TypeScript source, requested in review.
@github-actions github-actions Bot added the size/xl PR size: XL label Jul 30, 2026
@github-actions github-actions Bot added size/xl PR size: XL and removed size/xl PR size: XL labels Jul 30, 2026
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

4 participants