feat: add memory primitive#2366
Conversation
c40ff59 to
15204a7
Compare
|
Assessment: Request Changes This PR introduces a well-designed Review Categories
The implementation quality is high — clean separation of concerns, good use of |
| description, | ||
| inputSchema, | ||
| callback: async (input) => { | ||
| const stores = input.stores != null ? input.stores.filter((name) => scopedNames.includes(name)) : scopedNames |
There was a problem hiding this comment.
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[]> |
There was a problem hiding this comment.
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). | ||
| */ |
There was a problem hiding this comment.
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 {} | ||
|
|
||
| /** |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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'),
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
hatch run prepareChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.