Skip to content

feat: add memory primitive#2366

Draft
opieter-aws wants to merge 3 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/memory-manager
Draft

feat: add memory primitive#2366
opieter-aws wants to merge 3 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/memory-manager

Conversation

@opieter-aws
Copy link
Copy Markdown
Contributor

Description

Related Issues

Documentation PR

Type of Change

Bug fix
New feature
Breaking change
Documentation update
Other (please describe):

Testing

How have you tested the change? Verify that the changes do not break functionality or introduce warnings in consuming repositories: agents-docs, agents-tools, agents-cli

  • I ran hatch run prepare

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 branch 2 times, most recently from c40ff59 to 15204a7 Compare May 29, 2026 13:47
@github-actions github-actions Bot added size/l and removed size/l labels May 29, 2026
@opieter-aws opieter-aws temporarily deployed to manual-approval May 29, 2026 13:47 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown
Contributor

Assessment: Request Changes

This PR introduces a well-designed MemoryManager primitive for the TypeScript SDK. The architecture is clean — implementing the Plugin interface, exposing tools via getTools(), and providing both programmatic and tool-based access. However, the PR description is empty and this introduces a new public API primitive that requires more thorough documentation and review.

Review Categories
  • API Review Process: This introduces a new public primitive (MemoryManager, MemoryStore, etc.) that customers will directly use. It needs needs-api-review label and the PR description should include use cases, example code, and API design rationale.
  • PR Description: The description template is unfilled — no motivation, no linked issue, no testing verification. For a feature of this scope, the "WHY" is critical for reviewers.
  • Code Quality: The implementation is solid with good error handling, graceful partial failures, and proper scoping. A few edge cases in the tool callbacks could be improved.
  • Testing: Comprehensive unit test coverage. A few minor gaps around search tool callback shape and error reporting to the model.

The implementation quality is high — clean separation of concerns, good use of Promise.allSettled for resilience, and thoughtful scoping logic in tools.

description,
inputSchema,
callback: async (input) => {
const stores = input.stores != null ? input.stores.filter((name) => scopedNames.includes(name)) : scopedNames
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.

Issue: When the model passes only out-of-scope store names, stores filters to [], and this.store(content, { stores: [] }) throws "no writable store matched" for every entry. The tool returns { stored: 0, failed: N } without any indication of why entries failed — the model has no way to understand that its store names were invalid.

Suggestion: Consider returning error details (e.g., { stored: 0, failed: N, error: "no matching writable stores in scope" }) or logging a warning when all stores are filtered out before attempting writes, to give the model actionable feedback.

/** Default max results per query for this store. Defaults to 3. */
readonly limit?: number
/** Search the store for entries matching the query, ordered by relevance. */
search(query: string, options?: SearchOptions): Promise<MemoryEntry[]>
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.

Issue: The MemoryStore interface requires only search as mandatory, with add being optional. However, there's no mechanism for stores to declare other capabilities (e.g., delete, update, list). If these are planned for follow-up PRs, that's fine, but it's worth noting.

More importantly: the search method only accepts SearchOptions with limit. Real vector stores typically need additional parameters like score_threshold, filter (metadata-based filtering), etc. The SearchOptions interface is very minimal.

Suggestion: Consider whether SearchOptions should be generic/extensible (e.g., SearchOptions & Record<string, unknown>) or if the intent is for store implementations to accept additional params through their own constructors. Documenting this design choice would help implementors.

* Memory manager for cross-session knowledge retrieval and storage.
* Manages one or more knowledge stores and exposes search/store tools.
* Accepts a {@link MemoryManager} instance or a {@link MemoryManagerConfig} object (auto-wrapped).
*/
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.

Issue: AgentConfig.sessionManager only accepts a SessionManager instance, while memoryManager accepts both MemoryManager | MemoryManagerConfig. This inconsistency could confuse users who expect the same pattern.

The config shorthand is nice for ergonomics, but since SessionManager doesn't offer this pattern, it raises the question: should this be consistent, or is there a deliberate reason for the difference?

Suggestion: Document in the PR description why the config-shorthand pattern was chosen here (presumably because MemoryManagerConfig is a simple data object unlike SessionManagerConfig which requires a storage instance). If this pattern is intended to be adopted for SessionManager too, note that as a follow-up.

*/
initAgent(_agent: LocalAgent): void {}

/**
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.

Issue: The initAgent method is a no-op with only a comment in the class TSDoc mentioning "deferred to a follow-up PR." There's no inline comment on the method itself explaining why it's empty, which could confuse future readers.

Suggestion: Add a brief inline comment:

initAgent(_agent: LocalAgent): void {
  // Context injection/extraction hooks deferred to follow-up PR.
}

description,
inputSchema,
callback: async (input) => {
const stores = input.stores != null ? input.stores.filter((name) => scopedNames.includes(name)) : scopedNames
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.

Issue: The search tool's stores parameter description says "do not pass an empty array" — this relies on the model following natural language instructions. If the model does pass [], the filter input.stores.filter(...) produces [], resulting in this.search(query, { stores: [] }) which searches no stores and silently returns empty results.

Suggestion: Consider normalizing [] to undefined (treat as "search all") in the callback, or explicitly handle this case with a warning. The instruction in the description is a workaround for a case that could be handled defensively in code.

throw new Error('MemoryManager: storeToolConfig targets no writable stores')
}

if (config.storeToolConfig === true && resolved.length > 1 && !toolConfig.stores) {
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.

Issue: The storeToolConfig === true guard that enforces explicit store specification for multiple writable stores is a good safety measure. However, the condition !toolConfig.stores is always true here because when storeToolConfig === true, toolConfig is always {} (from the ternary on line 83).

This isn't a bug, but the condition reads as if toolConfig.stores could be set in this branch, which it cannot. It would be clearer to simplify:

Suggestion:

if (config.storeToolConfig === true && resolved.length > 1) {
  throw new Error(
    'MemoryManager: storeToolConfig must specify `stores` when multiple writable stores are configured'
  )
}


const scopedNames = this._storeStores.map((s) => s.name)

const inputSchema = z.object({
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.

Issue: The entries schema allows an empty array (z.array(z.string())). If the model passes entries: [], the tool will return { stored: 0, failed: 0 } — a no-op that wastes a tool call.

Suggestion: Add .min(1) to prevent empty invocations:

entries: z.array(z.string()).min(1).describe('Data to store in long-term memory'),

@github-actions github-actions Bot removed the size/l label May 29, 2026
@yonib05 yonib05 added area-persistence Session management or checkpointing enhancement New feature or request python Pull requests that update python code labels May 29, 2026
@github-actions github-actions Bot removed the size/l label May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-persistence Session management or checkpointing enhancement New feature or request python Pull requests that update python code size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants