Skip to content

feat(memory): add memory manager#2544

Open
opieter-aws wants to merge 1 commit into
strands-agents:mainfrom
opieter-aws:opieter-aws/memory-manager-core
Open

feat(memory): add memory manager#2544
opieter-aws wants to merge 1 commit into
strands-agents:mainfrom
opieter-aws:opieter-aws/memory-manager-core

Conversation

@opieter-aws
Copy link
Copy Markdown
Contributor

Description

Adds the MemoryManager primitive: a construct that manages one or more memory store
backends and exposes search_memory / add_memory tools for agent-driven recall and
persistence, plus programmatic search() / add() methods.

What's included

  • MemoryManager (implements Plugin): registers its tools via getTools(), and
    aggregates any tools a store exposes via MemoryStore.getTools().
  • MemoryStore interface: every store is searchable; a required writable flag declares
    whether it accepts writes. search_memory targets all stores; add_memory targets only
    writable stores. The constructor fails fast if a store is writable: true without add().
  • MemoryStoreConfig: shared base config that concrete store implementations extend.
  • Tool store scoping: the model may target a subset of stores by name; out-of-scope names
    are dropped (with a warning), or an actionable error is thrown if all requested names
    are out of scope. Omitted/empty stores targets all in-scope stores.
  • Agent integration: new AgentConfig.memoryManager field accepting a MemoryManager
    instance or a MemoryManagerConfig object (auto-wrapped).

Scope (intentionally minimal)
This PR ships the MemoryManager and MemoryStore interface only, to unblock downstream
consumers (e.g. AgentCore memory) from building against a stable interface. Two pieces are
deferred to dedicated follow-up PRs:

  • Context injection — passive pre-model-call memory injection. Deferred so it can be
    built on the upcoming middleware system (sdk-typescript#1068) rather than lifecycle hooks,
    which avoids mutating durable session history.
  • BedrockKnowledgeBaseStore — a concrete store implementation, tracked separately
    (needs its own tests + optional AWS peer-dependency wiring).

initAgent is currently a no-op; tools auto-register through the PluginRegistry via
getTools(). Will be populated when adding injection and extraction.

Related Issues

#2393

Documentation PR

No new docs in this PR. A documentation PR will accompany the injection follow-up once the
full feature surface (injection + a shipped store backend) is finalized.

Type of Change

New feature

Testing

Comprehensive unit tests in strands-ts/src/memory/__tests__/memory-manager.test.ts (50
tests) cover: constructor validation (empty stores, duplicate names, writable-without-add,
add-tool enablement), tool registration, store-tool scoping (in-scope/out-of-scope/partial,
omit-vs-empty), search()/add() fan-out and partial-failure handling, store-provided
tool aggregation, and AgentConfig auto-wrapping.

  • I ran hatch run prepare (N/A — TypeScript package; ran npm test / type-check /
    lint / format:check instead)

Checklist

  • I have read the CONTRIBUTING document
  • 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.

@opieter-aws opieter-aws force-pushed the opieter-aws/memory-manager-core branch from ce8787d to 97a0c1d Compare June 1, 2026 21:22
@opieter-aws opieter-aws added the needs-api-review Makes changes to the public API surface label Jun 1, 2026
@opieter-aws opieter-aws marked this pull request as ready for review June 1, 2026 21:23
throw new Error('MemoryManager: no writable store matched')
}

const settled = await Promise.allSettled(writableStores.map((s) => s.add!(content, options?.metadata)))
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.

We have 2 paths here as I see it:

  1. awaiting at the callsite like we see here. What we gain from this is the lines below which is observability into the outcomes of the calls. The implicit invariant we want with this approach is that every add implementer fast returns and then fire-and-forgets the network call/API call.

It can also be noted that the fast return could also signal a success and then silently fail later

  1. We void not await at this callsite. Then we are guaranteed to not block the thread no matter what each add method does.


const results: MemoryEntry[] = []
for (let i = 0; i < settled.length; i++) {
const r = settled[i]!
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.

nit: here and elsewhere I prefer to avoid the single char names

* defaults on the store.
*/
export interface SearchOptions {
/** Maximum number of results to return. */
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.

this is representing the max per store, so lets make that clear in docstring and/or var name

return found
})
} else {
writableStores = this._config.stores.filter((s) => s.writable)
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.

Isn't it this.addStores here?

* ```
*/
export class MemoryManager implements Plugin {
readonly name = 'strands:memory-manager'
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.

Re-iterating offline conversation that we intend to make the manager unique per agent


private _createAddTool(config: MemoryToolConfig): Tool {
let description = config.description ?? ADD_TOOL_DESCRIPTION
const storeDescriptions = this._addStores.filter((s) => s.description).map((s) => `- ${s.name}: ${s.description}`)
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.

maybe it is nice to consolidate the description appending logic here and above maybe inline is better

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

Labels

needs-api-review Makes changes to the public API surface size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants