Skip to content

feat: add local memory store - #2859

Merged
opieter-aws merged 2 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/memory-store-applications
Jun 26, 2026
Merged

opieter-aws merged 2 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/memory-store-applications

Conversation

@opieter-aws

@opieter-aws opieter-aws commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Description

The memory feature ships only one concrete MemoryStore: BedrockKnowledgeBaseStore, which requires an AWS account, a provisioned Knowledge Base, and a data source. That gates the first experience of memory behind cloud setup — a developer can't try MemoryManager, search_memory/add_memory, context injection, or extraction without standing up infrastructure first.

This adds LocalMemoryStore, a zero-infrastructure MemoryStore shipped in both SDKs. It persists to disk by default (~/.strands/memory/<name>.json), so an agent remembers across restarts with no setup, and can be made ephemeral for tests and throwaway runs. MemoryManager is backend-agnostic — it talks to stores only through the MemoryStore interface — so this is purely additive: drop the store into stores: [...] and everything (tools, injection, client-side extraction) works unchanged.

A few design decisions worth calling out for review:

  • One class, not two. An ephemeral in-memory store and a persistent file store share identical recall logic; the only difference is whether add also flushes to disk. Rather than two classes, persistence is a single internal seam (persist flag → load-on-init + atomic flush). One construct to learn, one copy of the search code.
  • Persistent by default. Memory's value proposition is remembering across sessions, so the default that demonstrates the feature (survives a restart) is the right one; ephemeral is the opt-out.
  • Lexical recall, not semantic. v1 ranks by query-token overlap with a recency tiebreak — keyword matching with zero new dependencies. This differs from Bedrock's embedding-based search (a query word matches only the same word, not a synonym), and the docstrings say so explicitly.
  • Subpath export, mirroring BedrockKnowledgeBaseStore. Not added to the top-level barrel.

The store reads the whole file into memory and rewrites it atomically on each add, so it suits prototyping and personal memory (hundreds to low thousands of entries), not a production corpus — documented on the class. Writes within a process are serialized so concurrent adds can't clobber one another; cross-process writers are not coordinated.

Public API Changes

A new LocalMemoryStore, exported from a subpath in each SDK alongside the existing Bedrock store.

TypeScript (@strands-agents/sdk/vended-memory-stores/local):

import { Agent, MemoryManager } from '@strands-agents/sdk'
import { LocalMemoryStore } from '@strands-agents/sdk/vended-memory-stores/local'

// Persists to ~/.strands/memory/notes.json by default. Survives restarts.
const agent = new Agent({
  model,
  memoryManager: new MemoryManager({
    stores: [new LocalMemoryStore({ name: 'notes' })],
    addToolConfig: true,
  }),
})

// Ephemeral (tests / throwaway runs): nothing is written to disk.
new LocalMemoryStore({ name: 'notes', persist: false })
// Explicit location:
new LocalMemoryStore({ name: 'notes', path: './my-notes.json' })

Python (strands.vended_memory_stores.local):

from strands import Agent
from strands.memory.memory_manager import MemoryManager
from strands.vended_memory_stores.local import LocalMemoryStore

agent = Agent(
    model=model,
    memory_manager=MemoryManager(
        stores=[LocalMemoryStore(name="notes")],
        add_tool_config=True,
    ),
)

LocalMemoryStore(name="notes", persist=False)         # ephemeral
LocalMemoryStore(name="notes", path="./my-notes.json")  # explicit path

LocalMemoryStore defaults writable=true (the point is a store you can write to) and implements search + add. With an extraction config it uses the manager's client-side path (model extractor → add per fact); it does not implement addMessages. Constructor config: name (required), description, maxSearchResults/max_search_results, writable, extraction, persist (default true), path.

Related Issues

Documentation PR

team/designs/0011-memory-manager.md still sketches this store under the name InMemoryMemoryStore, imported from the top-level barrel. The shipped store is LocalMemoryStore (persists by default, so "InMemory" would mislead) exported via subpath (matching BedrockKnowledgeBaseStore). The design doc and any how-to/example docs should be reconciled in a follow-up; no site/ docs are included here.

Type of Change

New feature

Testing

Unit suites added in both SDKs covering: persistence round-trip across a fresh instance ("survives restart"), ephemeral mode (no file written, a new instance forgets), lexical ranking + recency tiebreak, empty/token-less query, empty/non-writable add errors, content dedup, maxSearchResults cap + <1 validation, and corrupt/missing-file handling. Each SDK also has MemoryManager integration tests asserting the manager stamps storeName, the add API writes through, and a client-side extraction run persists the extracted fact.

Beyond the automated suites, I exercised the store end-to-end in both SDKs across two separate processes: one writes a fact, a second (fresh process) recalls it from the on-disk JSON — confirming the across-restart behavior, not just in-process mocks. I also confirmed the new subpath export specifier resolves and the TS browser bundle still builds (the store's node:* imports are dynamic).

  • I ran hatch run prepare

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@github-actions github-actions Bot added size/xl enhancement New feature or request area-persistence Session management or checkpointing labels Jun 17, 2026
@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@opieter-aws
opieter-aws force-pushed the opieter-aws/memory-store-applications branch from 6fd41fe to 5a5f6ee Compare June 17, 2026 19:26
@opieter-aws
opieter-aws marked this pull request as ready for review June 17, 2026 19:26
Comment thread strands-py/src/strands/vended_memory_stores/local/store.py
@github-actions

Copy link
Copy Markdown
Contributor

Issue: This introduces a new public class (LocalMemoryStore) that customers are expected to use directly, which per team/API_BAR_RAISING.md is at least a "moderate" change warranting an explicit API reviewer sign-off — but the PR doesn't carry the api/needs-review label. The API itself is thoughtfully prepared (use cases, signatures, exports, and design rationale are all in the description), so this is mostly a process gate.

A couple of API-design points worth an explicit reviewer decision while you're at it:

  • Subpath export vs. flat namespace. Exporting via vended-memory-stores/local mirrors BedrockKnowledgeBaseStore (good for consistency), but DECISIONS.md — "Prefer Flat Namespaces" pushes the other way for commonly-used functionality. Since this store is explicitly positioned as the first zero-setup memory experience, it's worth confirming the subpath (rather than the top-level barrel) is the intended discoverability tradeoff.
  • Design doc divergence. team/designs/0011-memory-manager.md still sketches this as InMemoryMemoryStore imported from the top-level barrel. You've called this out in the description and deferred reconciliation to a follow-up — flagging so the API reviewer is aware the shipped surface intentionally diverges from the approved design.

Suggestion: Add api/needs-review and get an API reviewer to confirm the name/export/default-persistence decisions before merge (CI gates merge on api/review-complete).

Comment thread strands-py/src/strands/vended_memory_stores/local/store.py
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

A polished, well-documented addition that genuinely lowers the barrier to trying memory — zero-infra, cross-SDK parity, and the design rationale in the description is excellent. The main action item is process (API-review label), not code; the inline notes are minor.

Review Categories
  • API process: New public LocalMemoryStore is at least a "moderate" change per API_BAR_RAISING.md but lacks the api/needs-review label; the subpath-export and persistent-by-default decisions (and the divergence from design doc 0011's InMemoryMemoryStore) deserve an explicit reviewer sign-off.
  • Concurrency / performance: Python does blocking file I/O inside async methods and rewrites the whole file per add — fine for the documented prototyping scale, worth considering asyncio.to_thread (consistent with the existing Bedrock store's blocking pattern).
  • Documentation: Feature is user-facing but site/ docs + example checkboxes are unchecked and the design doc reconciliation is deferred — acknowledged in the description as follow-up.
  • Testing: Strong. Persistence round-trip, ephemeral mode, lexical ranking + recency tiebreak, corrupt/wrong-shape/malformed-file handling, dedup, concurrency, and cross-SDK interop are all covered in both SDKs; per-field assertions are appropriately limited to the non-deterministic id/createdAt.

Nice work — the single-class-with-persist-flag design and the cross-SDK on-disk format compatibility are particularly clean.

@opieter-aws
opieter-aws force-pushed the opieter-aws/memory-store-applications branch from 5a5f6ee to 69aba8a Compare June 18, 2026 15:41
@opieter-aws opieter-aws added the api/needs-review Makes changes to the public API surface label Jun 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Re-review of commit 69aba8a — thanks for the quick turnaround.

  • Reserved _relevanceScore key: addressed symmetrically in both SDKs.
  • ⏸️ Blocking async I/O (Python): unchanged, fine to defer (suggestion, consistent with the Bedrock store).
  • api/needs-review label: still missing — this remains the one item gating merge, since CI blocks merge until api/review-complete is applied. Once an API reviewer signs off on the name/export/default-persistence decisions, this is good to go.

Updated assessment unchanged: Comment — the code is in good shape; the remaining action is process, not code.

@opieter-aws
opieter-aws force-pushed the opieter-aws/memory-store-applications branch from 69aba8a to adaf59a Compare June 18, 2026 15:58
@github-actions

Copy link
Copy Markdown
Contributor

Re-review of commit adaf59a — all prior feedback is now addressed. 🎉

  • api/needs-review label applied — the CI merge gate is satisfied; this now just needs a human API reviewer's sign-off on the name / export / default-persistence decisions.
  • Flat-namespace concern resolvedLocalMemoryStore is now exported from the top-level vended_memory_stores barrel (lazy __getattr__), matching BedrockKnowledgeBaseStore. Both SDKs are internally consistent (Python barrel + subpath; TS subpath exports).
  • Reserved _relevanceScore key — documented in both SDKs (thread resolved).
  • Blocking I/O — accepted as intentional per your note (fire-and-forget via the memory manager, <10ms at <10k records, prototyping scope). Thread resolved.
  • Coverage 93.65% → 99.20% — the previously-uncovered __init__.py is now exercised; 36 Python tests + a 328-line TS suite.

Assessment: Approve (code-wise) — no remaining code-level action items. The only thing left is the API reviewer's actual approval, which the label now correctly routes. Nicely done.

@notowen333 notowen333 left a comment

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.

Just commenting on TS

Comment thread strands-ts/src/vended-memory-stores/local/store.ts
Comment thread strands-ts/src/vended-memory-stores/local/store.ts
Comment thread strands-ts/src/vended-memory-stores/local/store.ts
Comment thread strands-ts/src/vended-memory-stores/local/store.ts
Comment thread strands-ts/src/vended-memory-stores/local/store.ts
Comment thread strands-ts/src/vended-memory-stores/local/store.ts
@opieter-aws
opieter-aws force-pushed the opieter-aws/memory-store-applications branch from adaf59a to 83cc2f3 Compare June 23, 2026 15:12
@github-actions

Copy link
Copy Markdown
Contributor

Re-review of commit 83cc2f3 — no code changes since my last pass; Codecov now reports all modified lines covered. ✅ My two earlier threads remain resolved.

@notowen333 has raised some good design-discussion questions on the TS file — those are the author's to work through, so I won't pile on. Two quick context notes that may help that conversation, since the rationale is already implicit in the code:

  • Explicit-path validation (store.ts:243) — worth noting this is intentionally symmetric with the Python SDK, which also takes Path(path) directly without sanitization. Sanitization only guards the default name → path branch (to stop a store name escaping ~/.strands/memory). If you decide to validate explicit paths, doing it in both SDKs keeps the cross-SDK contract aligned.
  • Lazy path resolution (store.ts:272) — resolving once at construction is appealing, but _getPath is async specifically so the node:os / node:path imports can stay dynamic, keeping the module safe to bundle for the browser. A synchronous constructor resolve would pull those Node built-ins into the module's top-level imports.

No new action items from me — assessment stays Approve (code-wise), pending the human API + design discussion.

@github-actions

Copy link
Copy Markdown
Contributor

Re-review of commit d813879 ("Address comments") — nice round of cleanup. 👍

  • All 6 of @notowen333's TS design threads resolved with clear reasoning (sanitize-vs-validate separation, tokenizer dep tradeoff, cached _resolvedPath, JSON-as-simplest-with-room-to-extend). I find the responses sound — the sanitize/validate distinction in particular is the right call (you want spaces/accents allowed in the store name while still preventing path escape).
  • Store code is unchanged from 83cc2f3 (verified) — the threads were closed by discussion, not by churn. The new work is the responses plus the substantial site/ memory documentation.
  • My two Python threads remain resolved.

One light, non-blocking suggestion on the new docs: the memory overview.mdx "Stores" section covers store concepts generically and BedrockKnowledgeBaseStore gets its own page, but LocalMemoryStore — the zero-setup entry point this PR is built around — isn't called out anywhere in the user guide. A short snippet in the overview (e.g. "start here with no infra: new LocalMemoryStore({ name: 'notes' })") would let the headline feature be discoverable from the docs, not just the API. Easy to fold into the follow-up you already flagged.

Assessment: Approve (code-wise) — no code-level action items. Remaining items are the human API-review sign-off and the optional docs nicety above.

@opieter-aws
opieter-aws requested a review from notowen333 June 23, 2026 18:15
@pgrayy pgrayy added the api/review-complete An API Bar-raiser reviewed and accepted the APIs label Jun 26, 2026
@opieter-aws
opieter-aws merged commit 84fff9a into strands-agents:main Jun 26, 2026
39 of 42 checks passed
@opieter-aws
opieter-aws deleted the opieter-aws/memory-store-applications branch June 26, 2026 15:00
chaynabors added a commit to chaynabors/harness-sdk that referenced this pull request Jun 26, 2026
Both surfaced on the Python 3.14 linux job after a sequence of merges today:

1. test_format_request_maps_3gp_video_formats (from strands-agents#2306) called a non-existent
   _format_request; the public method is format_request.

2. test_ingests_extracted_facts_through_add (from strands-agents#2859) called the async
   MemoryManager.init_agent without awaiting it, so the extractor was never
   wired up and assert_called_once failed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api/needs-review Makes changes to the public API surface api/review-complete An API Bar-raiser reviewed and accepted the APIs area-persistence Session management or checkpointing enhancement New feature or request size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants