Skip to content

feat(memory): add memory manager#2544

Merged
opieter-aws merged 6 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/memory-manager-core
Jun 3, 2026
Merged

feat(memory): add memory manager#2544
opieter-aws merged 6 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/memory-manager-core

Conversation

@opieter-aws
Copy link
Copy Markdown
Contributor

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

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

Public API

AgentConfig.memoryManager

interface AgentConfig {
  // ...existing fields
  /** Accepts a MemoryManager instance or a config object (auto-wrapped). */
  memoryManager?: MemoryManager | MemoryManagerConfig
}

MemoryManager

class MemoryManager implements Plugin {
  readonly name: string // 'strands:memory-manager'
  constructor(config: MemoryManagerConfig)

  /** Search across configured stores (unscoped programmatic access). */
  search(query: string, options?: SearchMemoryOptions): Promise<MemoryEntry[]>
  /** Add content to writable stores (unscoped programmatic access). */
  add(content: string, options?: AddMemoryOptions): Promise<void>
  /** Registers search_memory / add_memory tools plus any store-provided tools. */
  getTools(): Tool[]
}

MemoryStore & MemoryStoreConfig

/** Shared identity/config fields — declared once, extended by both the store interface and concrete store configs. */
interface MemoryStoreConfig {
  readonly name: string                 // unique identifier, used for tool targeting
  readonly description?: string          // surfaced in tool descriptions
  readonly maxSearchResults?: number     // per-store default result limit
  readonly writable?: boolean            // caller intent; defaults to false
}

interface MemoryStore extends MemoryStoreConfig {
  readonly writable: boolean             // resolved (required at runtime)
  search(query: string, options?: SearchOptions): Promise<MemoryEntry[]>
  add?(content: string, metadata?: Record<string, JSONValue>): Promise<void> // required when writable
  getTools?(): Tool[]                    // optional store-provided tools
}

Supporting types

interface MemoryEntry {
  content: string
  store?: string                         // filled out by MemoryManager.search
  metadata?: Record<string, JSONValue>
}

interface SearchOptions { maxSearchResults?: number }                          // per-store search input
interface SearchMemoryOptions { maxSearchResults?: number; stores?: string[] } // manager-level
interface AddMemoryOptions { metadata?: Record<string, JSONValue>; stores?: string[] }
interface MemoryToolConfig { name?: string; description?: string }

interface MemoryManagerConfig {
  stores: MemoryStore[]
  searchToolConfig?: MemoryToolConfig | boolean  // default: true
  addToolConfig?: MemoryToolConfig | boolean     // default: false (opt-in)
}

Usage

// Config shorthand (auto-wrapped) — agent gains search_memory (+ add_memory) tools
const agent = new Agent({
  model,
  memoryManager: { stores: [myStore], addToolConfig: true },
})

// Class instance — also gives programmatic access
const memoryManager = new MemoryManager({ stores: [myStore], addToolConfig: true })
const agent = new Agent({ model, memoryManager })
await memoryManager.search('user preferences')
await memoryManager.add('User prefers dark mode')

// Multi-store with read-only store — search_memory queries all; add_memory only writable
new MemoryManager({
  stores: [personalStore, teamStore /* writable: false */],
  addToolConfig: true,
})

// Custom tool names
new MemoryManager({
  stores: [myStore],
  searchToolConfig: { name: 'recall' },
  addToolConfig: { name: 'remember' },
})

// Custom store
class MyStore implements MemoryStore {
  readonly name = 'my-store'
  readonly writable = true
  async search(query: string, options?: SearchOptions): Promise<MemoryEntry[]> { /* ... */ }
  async add(content: string, metadata?: Record<string, JSONValue>): Promise<void> { /* ... */ }
}

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
Comment thread strands-ts/src/memory/memory-manager.ts Outdated
Comment thread strands-ts/src/memory/memory-manager.ts Outdated
Comment thread strands-ts/src/memory/types.ts Outdated
Comment thread strands-ts/src/memory/memory-manager.ts Outdated
Comment thread strands-ts/src/memory/memory-manager.ts
Comment thread strands-ts/src/memory/memory-manager.ts Outdated
Copy link
Copy Markdown
Contributor

@mkmeral mkmeral left a comment

Choose a reason for hiding this comment

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

also my agent will post stuff soon, but mostly aligned with this i guess

Comment thread strands-ts/src/memory/types.ts
Comment thread strands-ts/src/memory/types.ts
Comment thread strands-ts/src/memory/types.ts Outdated
@agent-of-mkmeral
Copy link
Copy Markdown
Contributor

Did an API review pass on this (read the shipped code + tests, not just the description). Really clean, well-scoped work — fail-fast constructor validation, Promise.allSettled resilience, provenance stamping, and the _resolveToolTargets scoping logic are all nice. Two small behavior items and a naming suggestion below. None are blockers.

TL;DR

  1. add() writes to all writable stores by default — consider explicit-target when >1 writable store (duplicate-write footgun).
  2. Partial write reports success — surface per-store outcomes instead of resolving OK when some stores fail.
  3. Naming: SearchMemoryOptions could extend SearchOptions (kills a duplicated field), and the options types could follow the module's Memory*-prefix convention.

(Batch-vs-single on the add primitive: I know a separate optional batch method is planned, so that's backwards-compatible — not raising it here.)

1. Write-to-all-writable-stores by default — duplicate-write footgun

When options.stores is omitted, add() targets every writable store:

} else {
  writableStores = this._addStores   // ALL writable stores
}

With two writable stores (say personal + team), add('prefers dark mode') writes the same fact to both unless the model names one every call. Search-all-by-default is great (broad recall). Write-all-by-default duplicates facts and forces the model to specify stores on every write to avoid it.

Options: require explicit targeting when >1 writable store exists, or support a designated default-write store. (Single-writable-store setups are unaffected either way.)

2. Partial write success is reported as success

add() only throws when every store fails:

if (failures.length === writableStores.length) {
  throw new AggregateError(...)
}
// otherwise: logs a warning, resolves successfully

So writing to 2 stores where 1 throws → logger.warn(...) + the call resolves OK. The caller (and the model, via add_memory) believes it's durably saved when it's only in one store; a later search scoped to the failed store silently misses it. The add_memory tool's { stored, failed } counts entries (an entry is stored if ≥1 store accepted it), so per-store partial failure is invisible there too.

This ties into the await/observability thread on this PR — since the manager already awaits for exactly this reason, it'd be good to propagate per-store outcomes (e.g. a { store, ok, reason }[] summary) rather than collapse partial into success. At minimum, documenting the current semantics would help.

3. Naming — options types vs the module convention

Everything in the module is Memory*-prefixed, noun-first (MemoryEntry, MemoryStore, MemoryStoreConfig, MemoryToolConfig, MemoryManagerConfig). The three options types diverge:

Type Prefix Word order
MemoryStore, MemoryEntry, … Memory* noun-first
SearchOptions none verb-first
SearchMemoryOptions Memory infix verb-first
AddMemoryOptions Memory infix verb-first

The main one: SearchMemoryOptions is a strict superset of SearchOptions (same maxSearchResults + a routing stores?) but redeclares the shared field independently:

export interface SearchOptions {       maxSearchResults?: number }
export interface SearchMemoryOptions { maxSearchResults?: number; stores?: string[] }  // dup

This is the same drift MemoryStoreConfig was created to avoid ("declared once, extended by both"). Making it explicit removes the duplicate and expresses the is-a relationship:

export interface SearchOptions {                              // store primitive
  maxSearchResults?: number
}
export interface MemorySearchOptions extends SearchOptions {  // manager adds routing
  stores?: string[]
}

Suggested scheme: keep SearchOptions as the un-prefixed store primitive (input to MemoryStore.search); rename the manager-level ones to MemorySearchOptions extends SearchOptions and AddMemoryOptionsMemoryAddOptions. Then Memory* consistently means "manager layer" and un-prefixed means "store primitive."

(There's no store-level AddOptions twin since the write signature is add?(content, metadata?) with no options object — so MemoryAddOptions has nothing to extend, which is fine; might be worth a one-line note so the asymmetry doesn't read as an oversight.)

Tiny extra: MemoryEntry.store?: string holds a store name, not a MemoryStorestoreName would be unambiguous since it's on the model-visible result shape.

Happy to open inline suggestions or a small follow-up if any of these are useful — your call on what's in scope for this PR vs. the injection follow-up.

Comment thread strands-ts/src/memory/memory-manager.ts
Comment thread strands-ts/src/memory/types.ts Outdated
Comment thread strands-ts/src/memory/types.ts Outdated
Comment thread strands-ts/src/memory/types.ts
Comment thread strands-ts/src/memory/memory-manager.ts Outdated
@opieter-aws opieter-aws merged commit 1d9ce24 into strands-agents:main Jun 3, 2026
20 of 23 checks passed
@opieter-aws opieter-aws deleted the opieter-aws/memory-manager-core branch June 3, 2026 13:53
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.

5 participants