@betterdb/agent-memory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026-present BetterDB Inc.
2
+
3
+ Portions of this software are licensed as follows:
4
+
5
+ - All content residing under the "doc/" directory of this repository is licensed under the "Creative Commons: CC BY-SA 4.0 license".
6
+
7
+ - All content that resides under the "proprietary/" directory of this repository, if that directory exists, is licensed under the license defined in "proprietary/LICENSE".
8
+
9
+ - All third-party components incorporated into the BetterDB Software are licensed under the original license provided by the owner of the applicable component.
10
+
11
+ - Content outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
12
+
13
+ MIT License
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @betterdb/agent-memory
2
+
3
+ Standalone agent memory for [Valkey](https://valkey.io/): the short-term caching tiers from [`@betterdb/agent-cache`](../agent-cache/) plus a semantic long-term `MemoryStore` backed by [Valkey Search](https://valkey.io/topics/search/) (`FT.*`). Store memories with `remember()`, retrieve the most relevant ones with `recall()` (semantic similarity blended with recency and importance), and keep stores bounded with TTLs, capacity eviction, and `consolidate()`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @betterdb/agent-memory iovalkey
9
+ ```
10
+
11
+ Requires a Valkey server with the [Valkey Search](https://valkey.io/topics/search/) module loaded (for the `FT.*` commands), and an embedding function you provide.
12
+
13
+ ## Quick start
14
+
15
+ The `AgentMemory` facade wires the short-term cache tiers and the long-term memory store together over a single client and name:
16
+
17
+ ```ts
18
+ import Valkey from 'iovalkey';
19
+ import { AgentMemory } from '@betterdb/agent-memory';
20
+
21
+ const client = new Valkey('redis://localhost:6379');
22
+
23
+ const agent = new AgentMemory({
24
+ client,
25
+ name: 'my_agent',
26
+ embedFn: async (text) => embed(text), // returns number[]
27
+ });
28
+
29
+ // Create the vector index and register discovery markers (idempotent).
30
+ await agent.initialize();
31
+
32
+ // Long-term memory:
33
+ await agent.memory.remember('User prefers dark mode', {
34
+ agentId: 'assistant',
35
+ importance: 0.8,
36
+ tags: ['preferences'],
37
+ });
38
+
39
+ const hits = await agent.memory.recall('what theme does the user like?', {
40
+ agentId: 'assistant',
41
+ k: 5,
42
+ });
43
+
44
+ // Short-term cache tiers (from @betterdb/agent-cache):
45
+ agent.llm;
46
+ agent.tool;
47
+ agent.session;
48
+
49
+ await agent.close();
50
+ ```
51
+
52
+ You can also use the `MemoryStore` directly, without the cache tiers:
53
+
54
+ ```ts
55
+ import { MemoryStore } from '@betterdb/agent-memory';
56
+
57
+ const memory = new MemoryStore({ client, name: 'my_agent', embedFn });
58
+ await memory.ensureIndex();
59
+ ```
60
+
61
+ ## MemoryStore API
62
+
63
+ - `ensureIndex()` — create the `{name}:mem:idx` vector index if absent (idempotent). Resolves the vector dimension from `embedFn`.
64
+ - `remember(content, options?)` — embed and store a memory; returns its id. Options: `importance` (0..1), `tags`, `ttl` (seconds), and scope (`threadId`, `agentId`, `namespace`).
65
+ - `recall(query, options?)` — semantic search scoped by `threadId`/`agentId`/`namespace`/`tags`, ranked by a composite of similarity, recency (half-life decay), and importance. Returns `MemoryHit[]`. Recalled memories are reinforced (last-access + access-count bumped) unless `reinforce: false`.
66
+ - `forget(id)` — delete a single memory by id.
67
+ - `forgetByScope(scope)` — delete all memories matching a scope and/or tags.
68
+ - `consolidate(options)` — summarize a set of memories (via a `summarize` callback) into one new memory and optionally delete the sources. Select candidates by scope, tags, `olderThanSeconds`, or `maxImportance`.
69
+ - `currentConfig()` / `refreshConfig()` — read the live recall/eviction tunables; with `configRefresh` enabled the store periodically re-reads them from `{name}:__mem_config`.
70
+ - `close()` — stop the config-refresh timer and tear down discovery heartbeats.
71
+
72
+ ### Scoring & capacity
73
+
74
+ Recall ranks by `compositeScore` — a weighted blend of similarity, recency (true half-life decay), and importance. Defaults are tunable via `MemoryStoreOptions` (`weights`, `halfLifeSeconds`, `defaultThreshold`) or live via config refresh. Set `maxItemsPerScope` to cap memories per scope; over-capacity writes evict the lowest-scoring items (importance + recency).
75
+
76
+ ## Observability
77
+
78
+ Set `telemetry: { registry }` to register Prometheus metrics (`agent_memory_*`: items, recall total/hits/empty/latency, embedding calls, evictions, consolidations) and OpenTelemetry spans for each operation. With `discovery` enabled (default in the facade), the store publishes a marker to the shared `__betterdb:caches` registry so Monitor can auto-discover it.
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,28 @@
1
+ import { AgentCache, type AgentCacheOptions } from '@betterdb/agent-cache';
2
+ import { MemoryStore, type MemoryDiscoveryConfig, type MemoryConfigRefreshConfig } from './MemoryStore';
3
+ import type { RecallWeights } from './compositeScore';
4
+ import type { EmbedFn } from './types';
5
+ export interface AgentMemoryConfig {
6
+ defaultThreshold?: number;
7
+ recall?: {
8
+ weights?: RecallWeights;
9
+ halfLifeSeconds?: number;
10
+ };
11
+ maxItemsPerScope?: number;
12
+ discovery?: boolean | MemoryDiscoveryConfig;
13
+ configRefresh?: boolean | MemoryConfigRefreshConfig;
14
+ }
15
+ export interface AgentMemoryOptions extends AgentCacheOptions {
16
+ embedFn: EmbedFn;
17
+ memory?: AgentMemoryConfig;
18
+ }
19
+ export declare class AgentMemory {
20
+ readonly llm: AgentCache['llm'];
21
+ readonly tool: AgentCache['tool'];
22
+ readonly session: AgentCache['session'];
23
+ readonly memory: MemoryStore;
24
+ private readonly cache;
25
+ constructor(options: AgentMemoryOptions);
26
+ initialize(): Promise<void>;
27
+ close(): Promise<void>;
28
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentMemory = void 0;
4
+ const agent_cache_1 = require("@betterdb/agent-cache");
5
+ const MemoryStore_1 = require("./MemoryStore");
6
+ const DEFAULT_NAME = 'betterdb_ac';
7
+ class AgentMemory {
8
+ llm;
9
+ tool;
10
+ session;
11
+ memory;
12
+ cache;
13
+ constructor(options) {
14
+ if (typeof options.embedFn !== 'function') {
15
+ throw new Error('AgentMemory requires an embedFn to back the memory tier');
16
+ }
17
+ // Resolve the name once and hand the same value to both tiers so their key
18
+ // prefixes, discovery markers, and stats keys can never drift apart.
19
+ const name = options.name ?? DEFAULT_NAME;
20
+ this.cache = new agent_cache_1.AgentCache({ ...options, name });
21
+ this.llm = this.cache.llm;
22
+ this.tool = this.cache.tool;
23
+ this.session = this.cache.session;
24
+ const memory = options.memory ?? {};
25
+ this.memory = new MemoryStore_1.MemoryStore({
26
+ // AgentCacheOptions.client doesn't surface the `.call` method MemoryStore
27
+ // needs; a real ioredis/iovalkey client has it, so we assert the contract
28
+ // here. A method-only client/mock would compile but fail at runtime.
29
+ client: options.client,
30
+ name,
31
+ embedFn: options.embedFn,
32
+ defaultThreshold: memory.defaultThreshold,
33
+ weights: memory.recall?.weights,
34
+ halfLifeSeconds: memory.recall?.halfLifeSeconds,
35
+ maxItemsPerScope: memory.maxItemsPerScope,
36
+ // The facade is the batteries-included product: discover the memory tier
37
+ // alongside the cache tiers by default, unless explicitly disabled.
38
+ discovery: memory.discovery ?? true,
39
+ configRefresh: memory.configRefresh,
40
+ telemetry: options.telemetry?.registry ? { registry: options.telemetry.registry } : undefined,
41
+ });
42
+ }
43
+ async initialize() {
44
+ // Create the memory index before discovery so a freshly constructed facade
45
+ // is immediately usable for remember/recall without the caller hand-rolling
46
+ // the FT index. A create failure surfaces — the tier is unusable without it.
47
+ await this.memory.ensureIndex();
48
+ // Surface a discovery name-collision from either tier: awaiting
49
+ // ensureDiscoveryReady() is AgentCache's documented strict collision check,
50
+ // and the memory tier already propagates, so the cache side isn't swallowed.
51
+ await Promise.all([
52
+ this.cache.ensureDiscoveryReady(),
53
+ this.memory.ensureDiscoveryReady(),
54
+ ]);
55
+ }
56
+ async close() {
57
+ // Tear down both tiers even if one fails, so timers and heartbeats can't leak.
58
+ try {
59
+ await this.memory.close();
60
+ }
61
+ finally {
62
+ await this.cache.shutdown();
63
+ }
64
+ }
65
+ }
66
+ exports.AgentMemory = AgentMemory;
@@ -0,0 +1,81 @@
1
+ import { type RecallWeights } from './compositeScore';
2
+ import { type MemoryTelemetryOptions } from './telemetry';
3
+ import type { ConsolidateOptions, ConsolidateResult, EmbedFn, MemoryHit, MemoryScope, MemoryStoreClient, RecallOptions, RememberOptions } from './types';
4
+ export interface MemoryDiscoveryConfig {
5
+ version?: string;
6
+ heartbeatIntervalMs?: number;
7
+ }
8
+ export interface MemoryConfigRefreshConfig {
9
+ enabled?: boolean;
10
+ intervalMs?: number;
11
+ }
12
+ export interface MemoryConfigSnapshot {
13
+ threshold: number;
14
+ weights: RecallWeights;
15
+ halfLifeSeconds: number;
16
+ maxItemsPerScope?: number;
17
+ }
18
+ export interface MemoryStoreOptions {
19
+ client: MemoryStoreClient;
20
+ name: string;
21
+ embedFn: EmbedFn;
22
+ defaultThreshold?: number;
23
+ weights?: RecallWeights;
24
+ halfLifeSeconds?: number;
25
+ maxItemsPerScope?: number;
26
+ discovery?: boolean | MemoryDiscoveryConfig;
27
+ configRefresh?: boolean | MemoryConfigRefreshConfig;
28
+ telemetry?: MemoryTelemetryOptions;
29
+ }
30
+ export declare class MemoryStore {
31
+ private readonly client;
32
+ private readonly name;
33
+ private readonly embedFn;
34
+ private defaultThreshold;
35
+ private weights;
36
+ private halfLifeSeconds;
37
+ private maxItemsPerScope?;
38
+ private readonly initialThreshold;
39
+ private readonly initialWeights;
40
+ private readonly initialHalfLifeSeconds;
41
+ private readonly initialMaxItemsPerScope?;
42
+ private readonly configKey;
43
+ private configRefreshHandle;
44
+ private readonly discovery;
45
+ private discoveryReady;
46
+ private readonly telemetry;
47
+ private readonly storeLabels;
48
+ private dims?;
49
+ constructor(options: MemoryStoreOptions);
50
+ currentConfig(): MemoryConfigSnapshot;
51
+ refreshConfig(): Promise<void>;
52
+ private startConfigRefresh;
53
+ private applyConfig;
54
+ private createDiscovery;
55
+ ensureDiscoveryReady(): Promise<void>;
56
+ close(): Promise<void>;
57
+ /**
58
+ * Create the `{name}:mem:idx` vector index if it does not already exist.
59
+ * Idempotent — an existing index is left untouched. Resolves the vector
60
+ * dimension from `embedFn` when it has not been observed yet. Call once
61
+ * before the first remember/recall; the AgentMemory facade does this in
62
+ * initialize().
63
+ */
64
+ ensureIndex(): Promise<void>;
65
+ recall(query: string, options?: RecallOptions): Promise<MemoryHit[]>;
66
+ private recordRecall;
67
+ private traced;
68
+ private reinforce;
69
+ forget(id: string): Promise<boolean>;
70
+ forgetByScope(scope: MemoryScope & {
71
+ tags?: string[];
72
+ }): Promise<number>;
73
+ private writeMemory;
74
+ remember(content: string, options?: RememberOptions): Promise<string>;
75
+ consolidate(options: ConsolidateOptions): Promise<ConsolidateResult>;
76
+ private runConsolidate;
77
+ private writeRecord;
78
+ private enforceCapacity;
79
+ private resolveDims;
80
+ private embed;
81
+ }