@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 +19 -0
- package/README.md +82 -0
- package/dist/AgentMemory.d.ts +28 -0
- package/dist/AgentMemory.js +66 -0
- package/dist/MemoryStore.d.ts +81 -0
- package/dist/MemoryStore.js +583 -0
- package/dist/buildMemoryIndex.d.ts +4 -0
- package/dist/buildMemoryIndex.js +60 -0
- package/dist/buildMemoryRecord.d.ts +6 -0
- package/dist/buildMemoryRecord.js +47 -0
- package/dist/buildRecallQuery.d.ts +11 -0
- package/dist/buildRecallQuery.js +50 -0
- package/dist/compositeScore.d.ts +21 -0
- package/dist/compositeScore.js +23 -0
- package/dist/discovery.d.ts +48 -0
- package/dist/discovery.js +152 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +33 -0
- package/dist/parseMemoryItem.d.ts +3 -0
- package/dist/parseMemoryItem.js +33 -0
- package/dist/selectEvictions.d.ts +17 -0
- package/dist/selectEvictions.js +33 -0
- package/dist/telemetry.d.ts +24 -0
- package/dist/telemetry.js +77 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +2 -0
- package/package.json +47 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildMemoryRecord = buildMemoryRecord;
|
|
4
|
+
const valkey_search_kit_1 = require("@betterdb/valkey-search-kit");
|
|
5
|
+
const DEFAULT_IMPORTANCE = 0.5;
|
|
6
|
+
function buildMemoryRecord(name, id, content, vector, options, now) {
|
|
7
|
+
const importance = options.importance ?? DEFAULT_IMPORTANCE;
|
|
8
|
+
if (!Number.isFinite(importance) || importance < 0 || importance > 1) {
|
|
9
|
+
throw new Error(`importance must be a finite number in [0, 1], got: ${String(options.importance)}`);
|
|
10
|
+
}
|
|
11
|
+
const fields = [
|
|
12
|
+
'content',
|
|
13
|
+
content,
|
|
14
|
+
'vector',
|
|
15
|
+
(0, valkey_search_kit_1.encodeFloat32)(vector),
|
|
16
|
+
'importance',
|
|
17
|
+
String(importance),
|
|
18
|
+
'created_at',
|
|
19
|
+
String(now),
|
|
20
|
+
'last_accessed_at',
|
|
21
|
+
String(now),
|
|
22
|
+
'access_count',
|
|
23
|
+
'0',
|
|
24
|
+
];
|
|
25
|
+
const tags = options.tags ?? [];
|
|
26
|
+
for (const tag of tags) {
|
|
27
|
+
if (tag.includes(',')) {
|
|
28
|
+
throw new Error(`Tag '${tag}' must not contain a comma; tags are stored comma-separated`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (tags.length > 0) {
|
|
32
|
+
fields.push('tags', tags.join(','));
|
|
33
|
+
}
|
|
34
|
+
if (options.threadId !== undefined) {
|
|
35
|
+
fields.push('threadId', options.threadId);
|
|
36
|
+
}
|
|
37
|
+
if (options.agentId !== undefined) {
|
|
38
|
+
fields.push('agentId', options.agentId);
|
|
39
|
+
}
|
|
40
|
+
if (options.namespace !== undefined) {
|
|
41
|
+
fields.push('namespace', options.namespace);
|
|
42
|
+
}
|
|
43
|
+
if (options.source !== undefined) {
|
|
44
|
+
fields.push('source', options.source);
|
|
45
|
+
}
|
|
46
|
+
return { key: `${name}:mem:${id}`, fields };
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MemoryScope } from './types';
|
|
2
|
+
export declare const SCORE_FIELD = "__score";
|
|
3
|
+
export declare const VECTOR_FIELD = "vector";
|
|
4
|
+
export declare function buildScopeFilter(scope: MemoryScope, tags: string[]): string;
|
|
5
|
+
export interface ConsolidateFilterOptions {
|
|
6
|
+
maxCreatedAt?: number;
|
|
7
|
+
maxImportance?: number;
|
|
8
|
+
excludeSource?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildConsolidateFilter(scope: MemoryScope, tags: string[], options: ConsolidateFilterOptions): string;
|
|
11
|
+
export declare function buildRecallQuery(k: number, scope: MemoryScope, tags: string[]): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VECTOR_FIELD = exports.SCORE_FIELD = void 0;
|
|
4
|
+
exports.buildScopeFilter = buildScopeFilter;
|
|
5
|
+
exports.buildConsolidateFilter = buildConsolidateFilter;
|
|
6
|
+
exports.buildRecallQuery = buildRecallQuery;
|
|
7
|
+
const valkey_search_kit_1 = require("@betterdb/valkey-search-kit");
|
|
8
|
+
exports.SCORE_FIELD = '__score';
|
|
9
|
+
exports.VECTOR_FIELD = 'vector';
|
|
10
|
+
function scopeClauses(scope, tags) {
|
|
11
|
+
const clauses = [];
|
|
12
|
+
if (scope.threadId !== undefined) {
|
|
13
|
+
clauses.push(`@threadId:{${(0, valkey_search_kit_1.escapeTag)(scope.threadId)}}`);
|
|
14
|
+
}
|
|
15
|
+
if (scope.agentId !== undefined) {
|
|
16
|
+
clauses.push(`@agentId:{${(0, valkey_search_kit_1.escapeTag)(scope.agentId)}}`);
|
|
17
|
+
}
|
|
18
|
+
if (scope.namespace !== undefined) {
|
|
19
|
+
clauses.push(`@namespace:{${(0, valkey_search_kit_1.escapeTag)(scope.namespace)}}`);
|
|
20
|
+
}
|
|
21
|
+
for (const tag of tags) {
|
|
22
|
+
clauses.push(`@tags:{${(0, valkey_search_kit_1.escapeTag)(tag)}}`);
|
|
23
|
+
}
|
|
24
|
+
return clauses;
|
|
25
|
+
}
|
|
26
|
+
function joinClauses(clauses) {
|
|
27
|
+
if (clauses.length === 0) {
|
|
28
|
+
return '*';
|
|
29
|
+
}
|
|
30
|
+
return `(${clauses.join(' ')})`;
|
|
31
|
+
}
|
|
32
|
+
function buildScopeFilter(scope, tags) {
|
|
33
|
+
return joinClauses(scopeClauses(scope, tags));
|
|
34
|
+
}
|
|
35
|
+
function buildConsolidateFilter(scope, tags, options) {
|
|
36
|
+
const clauses = scopeClauses(scope, tags);
|
|
37
|
+
if (options.maxCreatedAt !== undefined) {
|
|
38
|
+
clauses.push(`@created_at:[-inf ${options.maxCreatedAt}]`);
|
|
39
|
+
}
|
|
40
|
+
if (options.maxImportance !== undefined) {
|
|
41
|
+
clauses.push(`@importance:[-inf ${options.maxImportance}]`);
|
|
42
|
+
}
|
|
43
|
+
if (options.excludeSource !== undefined) {
|
|
44
|
+
clauses.push(`-@source:{${(0, valkey_search_kit_1.escapeTag)(options.excludeSource)}}`);
|
|
45
|
+
}
|
|
46
|
+
return joinClauses(clauses);
|
|
47
|
+
}
|
|
48
|
+
function buildRecallQuery(k, scope, tags) {
|
|
49
|
+
return `${buildScopeFilter(scope, tags)}=>[KNN ${k} @${exports.VECTOR_FIELD} $vec AS ${exports.SCORE_FIELD}]`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface RecallWeights {
|
|
2
|
+
similarity: number;
|
|
3
|
+
recency: number;
|
|
4
|
+
importance: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CompositeScoreParams {
|
|
7
|
+
similarity: number;
|
|
8
|
+
ageSeconds: number;
|
|
9
|
+
importance: number;
|
|
10
|
+
weights: RecallWeights;
|
|
11
|
+
halfLifeSeconds: number;
|
|
12
|
+
}
|
|
13
|
+
/** True half-life decay: 1 at age 0, 0.5 at one halfLifeSeconds, approaching 0 beyond. */
|
|
14
|
+
export declare function recencyDecay(ageSeconds: number, halfLifeSeconds: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* Weighted blend of semantic similarity, recency, and importance.
|
|
17
|
+
* Recency is a true half-life decay: 0.5 at one halfLifeSeconds.
|
|
18
|
+
*/
|
|
19
|
+
export declare function compositeScore(params: CompositeScoreParams): number;
|
|
20
|
+
/** Map cosine distance (0..2, lower = closer) to a 0..1 similarity score. */
|
|
21
|
+
export declare function similarityFromDistance(distance: number): number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recencyDecay = recencyDecay;
|
|
4
|
+
exports.compositeScore = compositeScore;
|
|
5
|
+
exports.similarityFromDistance = similarityFromDistance;
|
|
6
|
+
/** True half-life decay: 1 at age 0, 0.5 at one halfLifeSeconds, approaching 0 beyond. */
|
|
7
|
+
function recencyDecay(ageSeconds, halfLifeSeconds) {
|
|
8
|
+
return Math.exp((-Math.LN2 * ageSeconds) / halfLifeSeconds);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Weighted blend of semantic similarity, recency, and importance.
|
|
12
|
+
* Recency is a true half-life decay: 0.5 at one halfLifeSeconds.
|
|
13
|
+
*/
|
|
14
|
+
function compositeScore(params) {
|
|
15
|
+
const recency = recencyDecay(params.ageSeconds, params.halfLifeSeconds);
|
|
16
|
+
return (params.weights.similarity * params.similarity +
|
|
17
|
+
params.weights.recency * recency +
|
|
18
|
+
params.weights.importance * params.importance);
|
|
19
|
+
}
|
|
20
|
+
/** Map cosine distance (0..2, lower = closer) to a 0..1 similarity score. */
|
|
21
|
+
function similarityFromDistance(distance) {
|
|
22
|
+
return 1 - distance / 2;
|
|
23
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { MemoryStoreClient } from './types';
|
|
2
|
+
export declare const MEMORY_CACHE_TYPE = "agent_memory";
|
|
3
|
+
export declare const MEMORY_CAPABILITIES: string[];
|
|
4
|
+
export interface MemoryMarker {
|
|
5
|
+
type: typeof MEMORY_CACHE_TYPE;
|
|
6
|
+
prefix: string;
|
|
7
|
+
version: string;
|
|
8
|
+
protocol_version: number;
|
|
9
|
+
capabilities: string[];
|
|
10
|
+
stats_key: string;
|
|
11
|
+
started_at: string;
|
|
12
|
+
pid?: number;
|
|
13
|
+
hostname?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MemoryDiscoveryDeps {
|
|
16
|
+
client: MemoryStoreClient;
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
statsKey: string;
|
|
20
|
+
heartbeatIntervalMs?: number;
|
|
21
|
+
onWriteFailed?: () => void;
|
|
22
|
+
}
|
|
23
|
+
export declare class MemoryDiscovery {
|
|
24
|
+
private readonly client;
|
|
25
|
+
private readonly name;
|
|
26
|
+
private readonly version;
|
|
27
|
+
private readonly statsKey;
|
|
28
|
+
private readonly heartbeatIntervalMs;
|
|
29
|
+
private readonly markerField;
|
|
30
|
+
private readonly heartbeatKey;
|
|
31
|
+
private readonly startedAt;
|
|
32
|
+
private readonly onWriteFailed;
|
|
33
|
+
private heartbeatHandle;
|
|
34
|
+
private inFlightTick;
|
|
35
|
+
constructor(deps: MemoryDiscoveryDeps);
|
|
36
|
+
buildMarker(): MemoryMarker;
|
|
37
|
+
register(): Promise<void>;
|
|
38
|
+
stop(opts: {
|
|
39
|
+
deleteHeartbeat: boolean;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
tickHeartbeat(): Promise<void>;
|
|
42
|
+
private startHeartbeat;
|
|
43
|
+
private writeHeartbeat;
|
|
44
|
+
private writeMarker;
|
|
45
|
+
private safeHget;
|
|
46
|
+
private safeCall;
|
|
47
|
+
private checkCollision;
|
|
48
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemoryDiscovery = exports.MEMORY_CAPABILITIES = exports.MEMORY_CACHE_TYPE = void 0;
|
|
4
|
+
const node_os_1 = require("node:os");
|
|
5
|
+
const agent_cache_1 = require("@betterdb/agent-cache");
|
|
6
|
+
exports.MEMORY_CACHE_TYPE = 'agent_memory';
|
|
7
|
+
exports.MEMORY_CAPABILITIES = ['recall', 'consolidate', 'reinforce'];
|
|
8
|
+
class MemoryDiscovery {
|
|
9
|
+
client;
|
|
10
|
+
name;
|
|
11
|
+
version;
|
|
12
|
+
statsKey;
|
|
13
|
+
heartbeatIntervalMs;
|
|
14
|
+
markerField;
|
|
15
|
+
heartbeatKey;
|
|
16
|
+
startedAt;
|
|
17
|
+
onWriteFailed;
|
|
18
|
+
heartbeatHandle = null;
|
|
19
|
+
inFlightTick = null;
|
|
20
|
+
constructor(deps) {
|
|
21
|
+
this.client = deps.client;
|
|
22
|
+
this.name = deps.name;
|
|
23
|
+
this.version = deps.version;
|
|
24
|
+
this.statsKey = deps.statsKey;
|
|
25
|
+
this.heartbeatIntervalMs = deps.heartbeatIntervalMs ?? agent_cache_1.DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
26
|
+
// Namespace the marker under `{name}:mem` so a memory store and an
|
|
27
|
+
// agent-cache sharing the same name register distinct registry fields and
|
|
28
|
+
// heartbeat keys instead of clobbering each other.
|
|
29
|
+
this.markerField = `${deps.name}:mem`;
|
|
30
|
+
this.heartbeatKey = `${agent_cache_1.HEARTBEAT_KEY_PREFIX}${this.markerField}`;
|
|
31
|
+
this.startedAt = new Date().toISOString();
|
|
32
|
+
this.onWriteFailed = deps.onWriteFailed ?? (() => { });
|
|
33
|
+
}
|
|
34
|
+
buildMarker() {
|
|
35
|
+
const marker = {
|
|
36
|
+
type: exports.MEMORY_CACHE_TYPE,
|
|
37
|
+
prefix: this.name,
|
|
38
|
+
version: this.version,
|
|
39
|
+
protocol_version: agent_cache_1.PROTOCOL_VERSION,
|
|
40
|
+
capabilities: [...exports.MEMORY_CAPABILITIES],
|
|
41
|
+
stats_key: this.statsKey,
|
|
42
|
+
started_at: this.startedAt,
|
|
43
|
+
pid: process.pid,
|
|
44
|
+
hostname: (0, node_os_1.hostname)(),
|
|
45
|
+
};
|
|
46
|
+
return marker;
|
|
47
|
+
}
|
|
48
|
+
async register() {
|
|
49
|
+
// HGET-then-HSET is not atomic (TOCTOU); acceptable for best-effort
|
|
50
|
+
// discovery — a racing writer just means last-writer-wins on the marker.
|
|
51
|
+
const existing = await this.safeHget();
|
|
52
|
+
if (existing !== null) {
|
|
53
|
+
this.checkCollision(existing);
|
|
54
|
+
}
|
|
55
|
+
await this.writeMarker();
|
|
56
|
+
await this.safeCall(() => this.client.call('SET', agent_cache_1.PROTOCOL_KEY, String(agent_cache_1.PROTOCOL_VERSION), 'NX'));
|
|
57
|
+
await this.writeHeartbeat();
|
|
58
|
+
this.startHeartbeat();
|
|
59
|
+
}
|
|
60
|
+
async stop(opts) {
|
|
61
|
+
if (this.heartbeatHandle) {
|
|
62
|
+
clearInterval(this.heartbeatHandle);
|
|
63
|
+
this.heartbeatHandle = null;
|
|
64
|
+
}
|
|
65
|
+
// Wait out a tick already in flight so its heartbeat/marker writes can't
|
|
66
|
+
// land after the DEL below and make the store look alive post-shutdown.
|
|
67
|
+
if (this.inFlightTick) {
|
|
68
|
+
await this.inFlightTick;
|
|
69
|
+
}
|
|
70
|
+
if (!opts.deleteHeartbeat) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await this.client.call('DEL', this.heartbeatKey);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
this.onWriteFailed();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async tickHeartbeat() {
|
|
81
|
+
await this.writeHeartbeat();
|
|
82
|
+
await this.writeMarker();
|
|
83
|
+
// PROTOCOL_KEY is set once in register(); the NX SET is a guaranteed no-op
|
|
84
|
+
// on every subsequent tick, so it's not re-issued from the heartbeat path.
|
|
85
|
+
}
|
|
86
|
+
startHeartbeat() {
|
|
87
|
+
const handle = setInterval(() => {
|
|
88
|
+
this.inFlightTick = this.tickHeartbeat()
|
|
89
|
+
.catch(() => undefined)
|
|
90
|
+
.finally(() => {
|
|
91
|
+
this.inFlightTick = null;
|
|
92
|
+
});
|
|
93
|
+
}, this.heartbeatIntervalMs);
|
|
94
|
+
handle.unref?.();
|
|
95
|
+
this.heartbeatHandle = handle;
|
|
96
|
+
}
|
|
97
|
+
async writeHeartbeat() {
|
|
98
|
+
try {
|
|
99
|
+
await this.client.call('SET', this.heartbeatKey, new Date().toISOString(), 'EX', String(agent_cache_1.HEARTBEAT_TTL_SECONDS));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
this.onWriteFailed();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async writeMarker() {
|
|
106
|
+
let payload;
|
|
107
|
+
try {
|
|
108
|
+
payload = JSON.stringify(this.buildMarker());
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
this.onWriteFailed();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await this.safeCall(() => this.client.call('HSET', agent_cache_1.REGISTRY_KEY, this.markerField, payload));
|
|
115
|
+
}
|
|
116
|
+
async safeHget() {
|
|
117
|
+
try {
|
|
118
|
+
const value = await this.client.call('HGET', agent_cache_1.REGISTRY_KEY, this.markerField);
|
|
119
|
+
return value === null || value === undefined ? null : String(value);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
this.onWriteFailed();
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async safeCall(fn) {
|
|
127
|
+
try {
|
|
128
|
+
await fn();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
this.onWriteFailed();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
checkCollision(existingJson) {
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = JSON.parse(existingJson);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (parsed.type && parsed.type !== exports.MEMORY_CACHE_TYPE) {
|
|
143
|
+
// Reachable only if a non-agent_memory marker already occupies this field.
|
|
144
|
+
// The memory marker lives under `{name}:mem`, distinct from agent-cache's
|
|
145
|
+
// `{name}`, so the two tiers never collide here. Surface it with a visible
|
|
146
|
+
// warning rather than throwing into a swallowed registration promise;
|
|
147
|
+
// registration then proceeds last-writer-wins, matching agent-cache.
|
|
148
|
+
console.warn(`agent-memory discovery: field '${this.markerField}' already holds a '${parsed.type}' marker; overwriting`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.MemoryDiscovery = MemoryDiscovery;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from '@betterdb/agent-cache';
|
|
2
|
+
export { MemoryStore } from './MemoryStore';
|
|
3
|
+
export type { MemoryStoreOptions, MemoryDiscoveryConfig, MemoryConfigRefreshConfig, MemoryConfigSnapshot, } from './MemoryStore';
|
|
4
|
+
export { MemoryDiscovery, MEMORY_CACHE_TYPE, MEMORY_CAPABILITIES } from './discovery';
|
|
5
|
+
export type { MemoryDiscoveryDeps, MemoryMarker } from './discovery';
|
|
6
|
+
export { createMemoryTelemetry, DEFAULT_METRICS_PREFIX, DEFAULT_TRACER_NAME } from './telemetry';
|
|
7
|
+
export type { MemoryTelemetry, MemoryTelemetryOptions, MemoryMetrics } from './telemetry';
|
|
8
|
+
export { AgentMemory } from './AgentMemory';
|
|
9
|
+
export type { AgentMemoryOptions, AgentMemoryConfig } from './AgentMemory';
|
|
10
|
+
export type { EmbedFn, MemoryStoreClient, MemoryScope, RememberOptions, MemoryItem, RecallOptions, MemoryHit, ConsolidateOptions, ConsolidateResult, } from './types';
|
|
11
|
+
export { compositeScore, similarityFromDistance } from './compositeScore';
|
|
12
|
+
export type { RecallWeights, CompositeScoreParams } from './compositeScore';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.similarityFromDistance = exports.compositeScore = exports.AgentMemory = exports.DEFAULT_TRACER_NAME = exports.DEFAULT_METRICS_PREFIX = exports.createMemoryTelemetry = exports.MEMORY_CAPABILITIES = exports.MEMORY_CACHE_TYPE = exports.MemoryDiscovery = exports.MemoryStore = void 0;
|
|
18
|
+
__exportStar(require("@betterdb/agent-cache"), exports);
|
|
19
|
+
var MemoryStore_1 = require("./MemoryStore");
|
|
20
|
+
Object.defineProperty(exports, "MemoryStore", { enumerable: true, get: function () { return MemoryStore_1.MemoryStore; } });
|
|
21
|
+
var discovery_1 = require("./discovery");
|
|
22
|
+
Object.defineProperty(exports, "MemoryDiscovery", { enumerable: true, get: function () { return discovery_1.MemoryDiscovery; } });
|
|
23
|
+
Object.defineProperty(exports, "MEMORY_CACHE_TYPE", { enumerable: true, get: function () { return discovery_1.MEMORY_CACHE_TYPE; } });
|
|
24
|
+
Object.defineProperty(exports, "MEMORY_CAPABILITIES", { enumerable: true, get: function () { return discovery_1.MEMORY_CAPABILITIES; } });
|
|
25
|
+
var telemetry_1 = require("./telemetry");
|
|
26
|
+
Object.defineProperty(exports, "createMemoryTelemetry", { enumerable: true, get: function () { return telemetry_1.createMemoryTelemetry; } });
|
|
27
|
+
Object.defineProperty(exports, "DEFAULT_METRICS_PREFIX", { enumerable: true, get: function () { return telemetry_1.DEFAULT_METRICS_PREFIX; } });
|
|
28
|
+
Object.defineProperty(exports, "DEFAULT_TRACER_NAME", { enumerable: true, get: function () { return telemetry_1.DEFAULT_TRACER_NAME; } });
|
|
29
|
+
var AgentMemory_1 = require("./AgentMemory");
|
|
30
|
+
Object.defineProperty(exports, "AgentMemory", { enumerable: true, get: function () { return AgentMemory_1.AgentMemory; } });
|
|
31
|
+
var compositeScore_1 = require("./compositeScore");
|
|
32
|
+
Object.defineProperty(exports, "compositeScore", { enumerable: true, get: function () { return compositeScore_1.compositeScore; } });
|
|
33
|
+
Object.defineProperty(exports, "similarityFromDistance", { enumerable: true, get: function () { return compositeScore_1.similarityFromDistance; } });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseMemoryItem = parseMemoryItem;
|
|
4
|
+
function parseMemoryItem(name, hit) {
|
|
5
|
+
const prefix = `${name}:mem:`;
|
|
6
|
+
let id = hit.key;
|
|
7
|
+
if (hit.key.startsWith(prefix)) {
|
|
8
|
+
id = hit.key.slice(prefix.length);
|
|
9
|
+
}
|
|
10
|
+
const fields = hit.fields;
|
|
11
|
+
const item = {
|
|
12
|
+
id,
|
|
13
|
+
content: fields.content ?? '',
|
|
14
|
+
importance: parseFloat(fields.importance ?? '0'),
|
|
15
|
+
tags: fields.tags ? fields.tags.split(',') : [],
|
|
16
|
+
createdAt: parseInt(fields.created_at ?? '0', 10),
|
|
17
|
+
lastAccessedAt: parseInt(fields.last_accessed_at ?? '0', 10),
|
|
18
|
+
accessCount: parseInt(fields.access_count ?? '0', 10),
|
|
19
|
+
};
|
|
20
|
+
if (fields.source !== undefined) {
|
|
21
|
+
item.source = fields.source;
|
|
22
|
+
}
|
|
23
|
+
if (fields.threadId !== undefined) {
|
|
24
|
+
item.threadId = fields.threadId;
|
|
25
|
+
}
|
|
26
|
+
if (fields.agentId !== undefined) {
|
|
27
|
+
item.agentId = fields.agentId;
|
|
28
|
+
}
|
|
29
|
+
if (fields.namespace !== undefined) {
|
|
30
|
+
item.namespace = fields.namespace;
|
|
31
|
+
}
|
|
32
|
+
return item;
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RecallWeights } from './compositeScore';
|
|
2
|
+
export interface EvictionCandidate {
|
|
3
|
+
key: string;
|
|
4
|
+
importance: number;
|
|
5
|
+
lastAccessedAt: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SelectEvictionsOptions {
|
|
8
|
+
now: number;
|
|
9
|
+
halfLifeSeconds: number;
|
|
10
|
+
weights: RecallWeights;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Pick the keys to evict so that `maxItems` remain. Eviction blends importance
|
|
14
|
+
* with last-access recency (the recall weights, minus similarity, renormalized);
|
|
15
|
+
* lowest-scoring keys go first, ties broken toward the older last-access.
|
|
16
|
+
*/
|
|
17
|
+
export declare function selectEvictions(candidates: EvictionCandidate[], maxItems: number, options: SelectEvictionsOptions): string[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectEvictions = selectEvictions;
|
|
4
|
+
const compositeScore_1 = require("./compositeScore");
|
|
5
|
+
function evictionScore(candidate, options) {
|
|
6
|
+
const { weights } = options;
|
|
7
|
+
const denom = weights.importance + weights.recency;
|
|
8
|
+
if (denom === 0) {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
const ageSeconds = (options.now - candidate.lastAccessedAt) / 1000;
|
|
12
|
+
const recency = (0, compositeScore_1.recencyDecay)(ageSeconds, options.halfLifeSeconds);
|
|
13
|
+
return (weights.importance * candidate.importance + weights.recency * recency) / denom;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Pick the keys to evict so that `maxItems` remain. Eviction blends importance
|
|
17
|
+
* with last-access recency (the recall weights, minus similarity, renormalized);
|
|
18
|
+
* lowest-scoring keys go first, ties broken toward the older last-access.
|
|
19
|
+
*/
|
|
20
|
+
function selectEvictions(candidates, maxItems, options) {
|
|
21
|
+
const dropCount = candidates.length - Math.max(0, maxItems);
|
|
22
|
+
if (dropCount <= 0) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const ranked = candidates
|
|
26
|
+
.map((candidate) => ({
|
|
27
|
+
key: candidate.key,
|
|
28
|
+
score: evictionScore(candidate, options),
|
|
29
|
+
lastAccessedAt: candidate.lastAccessedAt,
|
|
30
|
+
}))
|
|
31
|
+
.sort((a, b) => a.score !== b.score ? a.score - b.score : a.lastAccessedAt - b.lastAccessedAt);
|
|
32
|
+
return ranked.slice(0, dropCount).map((entry) => entry.key);
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Tracer } from '@opentelemetry/api';
|
|
2
|
+
import { Counter, Gauge, Histogram, Registry } from 'prom-client';
|
|
3
|
+
export declare const DEFAULT_METRICS_PREFIX = "agent_memory";
|
|
4
|
+
export declare const DEFAULT_TRACER_NAME = "@betterdb/agent-memory";
|
|
5
|
+
export interface MemoryTelemetryOptions {
|
|
6
|
+
tracerName?: string;
|
|
7
|
+
metricsPrefix?: string;
|
|
8
|
+
registry?: Registry;
|
|
9
|
+
}
|
|
10
|
+
export interface MemoryMetrics {
|
|
11
|
+
items: Gauge<string>;
|
|
12
|
+
recallTotal: Counter<string>;
|
|
13
|
+
recallHits: Counter<string>;
|
|
14
|
+
recallEmpty: Counter<string>;
|
|
15
|
+
recallLatency: Histogram<string>;
|
|
16
|
+
embeddingCalls: Counter<string>;
|
|
17
|
+
evictions: Counter<string>;
|
|
18
|
+
consolidations: Counter<string>;
|
|
19
|
+
}
|
|
20
|
+
export interface MemoryTelemetry {
|
|
21
|
+
tracer: Tracer;
|
|
22
|
+
metrics: MemoryMetrics;
|
|
23
|
+
}
|
|
24
|
+
export declare function createMemoryTelemetry(options?: MemoryTelemetryOptions): MemoryTelemetry;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TRACER_NAME = exports.DEFAULT_METRICS_PREFIX = void 0;
|
|
4
|
+
exports.createMemoryTelemetry = createMemoryTelemetry;
|
|
5
|
+
const api_1 = require("@opentelemetry/api");
|
|
6
|
+
const prom_client_1 = require("prom-client");
|
|
7
|
+
exports.DEFAULT_METRICS_PREFIX = 'agent_memory';
|
|
8
|
+
exports.DEFAULT_TRACER_NAME = '@betterdb/agent-memory';
|
|
9
|
+
const RECALL_LATENCY_BUCKETS = [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0];
|
|
10
|
+
function getOrCreateCounter(registry, config) {
|
|
11
|
+
const existing = registry.getSingleMetric(config.name);
|
|
12
|
+
return existing
|
|
13
|
+
? existing
|
|
14
|
+
: new prom_client_1.Counter({ ...config, registers: [registry] });
|
|
15
|
+
}
|
|
16
|
+
function getOrCreateGauge(registry, config) {
|
|
17
|
+
const existing = registry.getSingleMetric(config.name);
|
|
18
|
+
return existing ? existing : new prom_client_1.Gauge({ ...config, registers: [registry] });
|
|
19
|
+
}
|
|
20
|
+
function getOrCreateHistogram(registry, config) {
|
|
21
|
+
const existing = registry.getSingleMetric(config.name);
|
|
22
|
+
return existing
|
|
23
|
+
? existing
|
|
24
|
+
: new prom_client_1.Histogram({ ...config, registers: [registry] });
|
|
25
|
+
}
|
|
26
|
+
function createMemoryTelemetry(options = {}) {
|
|
27
|
+
const registry = options.registry ?? prom_client_1.register;
|
|
28
|
+
const prefix = options.metricsPrefix ?? exports.DEFAULT_METRICS_PREFIX;
|
|
29
|
+
const tracer = api_1.trace.getTracer(options.tracerName ?? exports.DEFAULT_TRACER_NAME);
|
|
30
|
+
const labelNames = ['store_name'];
|
|
31
|
+
return {
|
|
32
|
+
tracer,
|
|
33
|
+
metrics: {
|
|
34
|
+
items: getOrCreateGauge(registry, {
|
|
35
|
+
name: `${prefix}_items`,
|
|
36
|
+
help: 'Approximate number of stored memories observed in-process',
|
|
37
|
+
labelNames,
|
|
38
|
+
}),
|
|
39
|
+
recallTotal: getOrCreateCounter(registry, {
|
|
40
|
+
name: `${prefix}_recall_total`,
|
|
41
|
+
help: 'Total recall queries',
|
|
42
|
+
labelNames,
|
|
43
|
+
}),
|
|
44
|
+
recallHits: getOrCreateCounter(registry, {
|
|
45
|
+
name: `${prefix}_recall_hits_total`,
|
|
46
|
+
help: 'Recall queries that returned at least one memory',
|
|
47
|
+
labelNames,
|
|
48
|
+
}),
|
|
49
|
+
recallEmpty: getOrCreateCounter(registry, {
|
|
50
|
+
name: `${prefix}_recall_empty_total`,
|
|
51
|
+
help: 'Recall queries that returned no memories',
|
|
52
|
+
labelNames,
|
|
53
|
+
}),
|
|
54
|
+
recallLatency: getOrCreateHistogram(registry, {
|
|
55
|
+
name: `${prefix}_recall_latency_seconds`,
|
|
56
|
+
help: 'Recall query latency in seconds',
|
|
57
|
+
labelNames,
|
|
58
|
+
buckets: RECALL_LATENCY_BUCKETS,
|
|
59
|
+
}),
|
|
60
|
+
embeddingCalls: getOrCreateCounter(registry, {
|
|
61
|
+
name: `${prefix}_embedding_calls_total`,
|
|
62
|
+
help: 'Total embedding function invocations',
|
|
63
|
+
labelNames,
|
|
64
|
+
}),
|
|
65
|
+
evictions: getOrCreateCounter(registry, {
|
|
66
|
+
name: `${prefix}_evictions_total`,
|
|
67
|
+
help: 'Total memories evicted for capacity',
|
|
68
|
+
labelNames,
|
|
69
|
+
}),
|
|
70
|
+
consolidations: getOrCreateCounter(registry, {
|
|
71
|
+
name: `${prefix}_consolidations_total`,
|
|
72
|
+
help: 'Total consolidation summaries created',
|
|
73
|
+
labelNames,
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RecallWeights } from './compositeScore';
|
|
2
|
+
export type EmbedFn = (text: string) => Promise<number[]>;
|
|
3
|
+
export interface MemoryStoreClient {
|
|
4
|
+
call(command: string, ...args: (string | Buffer | number)[]): Promise<unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface MemoryScope {
|
|
7
|
+
threadId?: string;
|
|
8
|
+
agentId?: string;
|
|
9
|
+
namespace?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface RememberOptions extends MemoryScope {
|
|
12
|
+
importance?: number;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
source?: string;
|
|
15
|
+
ttl?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface MemoryItem extends MemoryScope {
|
|
18
|
+
id: string;
|
|
19
|
+
content: string;
|
|
20
|
+
importance: number;
|
|
21
|
+
tags: string[];
|
|
22
|
+
source?: string;
|
|
23
|
+
createdAt: number;
|
|
24
|
+
lastAccessedAt: number;
|
|
25
|
+
accessCount: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RecallOptions extends MemoryScope {
|
|
28
|
+
k?: number;
|
|
29
|
+
threshold?: number;
|
|
30
|
+
tags?: string[];
|
|
31
|
+
weights?: RecallWeights;
|
|
32
|
+
reinforce?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface MemoryHit {
|
|
35
|
+
item: MemoryItem;
|
|
36
|
+
/**
|
|
37
|
+
* Raw KNN vector **distance** (cosine), not a similarity: lower means closer
|
|
38
|
+
* (a perfect match approaches 0). Despite the field name, do not assume
|
|
39
|
+
* higher is better — sort ascending if ranking by this alone. The composite
|
|
40
|
+
* `score` (higher is better) is the field to rank recall results by.
|
|
41
|
+
*/
|
|
42
|
+
similarity: number;
|
|
43
|
+
/** Composite recall score (similarity + recency + importance); higher is better. */
|
|
44
|
+
score: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ConsolidateOptions extends MemoryScope {
|
|
47
|
+
olderThanSeconds?: number;
|
|
48
|
+
maxImportance?: number;
|
|
49
|
+
summarize: (items: MemoryItem[]) => Promise<string>;
|
|
50
|
+
deleteSources?: boolean;
|
|
51
|
+
summaryImportance?: number;
|
|
52
|
+
tags?: string[];
|
|
53
|
+
}
|
|
54
|
+
export interface ConsolidateResult {
|
|
55
|
+
consolidated: number;
|
|
56
|
+
created: string[];
|
|
57
|
+
deleted: number;
|
|
58
|
+
}
|
package/dist/types.js
ADDED