@bip-krishna/agentmind 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/.agentmind/memory.db +0 -0
- package/.context-memory/memory.db +0 -0
- package/README.md +176 -0
- package/apps/dashboard/src/server.ts +369 -0
- package/cli/src/index.ts +163 -0
- package/dist/apps/dashboard/src/server.d.ts +6 -0
- package/dist/apps/dashboard/src/server.d.ts.map +1 -0
- package/dist/apps/dashboard/src/server.js +360 -0
- package/dist/apps/dashboard/src/server.js.map +1 -0
- package/dist/cli/src/index.d.ts +3 -0
- package/dist/cli/src/index.d.ts.map +1 -0
- package/dist/cli/src/index.js +162 -0
- package/dist/cli/src/index.js.map +1 -0
- package/dist/examples/advanced.d.ts +2 -0
- package/dist/examples/advanced.d.ts.map +1 -0
- package/dist/examples/advanced.js +41 -0
- package/dist/examples/advanced.js.map +1 -0
- package/dist/examples/basic.d.ts +2 -0
- package/dist/examples/basic.d.ts.map +1 -0
- package/dist/examples/basic.js +22 -0
- package/dist/examples/basic.js.map +1 -0
- package/dist/packages/core/src/consolidation.d.ts +27 -0
- package/dist/packages/core/src/consolidation.d.ts.map +1 -0
- package/dist/packages/core/src/consolidation.js +97 -0
- package/dist/packages/core/src/consolidation.js.map +1 -0
- package/dist/packages/core/src/events.d.ts +18 -0
- package/dist/packages/core/src/events.d.ts.map +1 -0
- package/dist/packages/core/src/events.js +13 -0
- package/dist/packages/core/src/events.js.map +1 -0
- package/dist/packages/core/src/extraction.d.ts +32 -0
- package/dist/packages/core/src/extraction.d.ts.map +1 -0
- package/dist/packages/core/src/extraction.js +65 -0
- package/dist/packages/core/src/extraction.js.map +1 -0
- package/dist/packages/core/src/index.d.ts +10 -0
- package/dist/packages/core/src/index.d.ts.map +1 -0
- package/dist/packages/core/src/index.js +7 -0
- package/dist/packages/core/src/index.js.map +1 -0
- package/dist/packages/core/src/memory-store.d.ts +36 -0
- package/dist/packages/core/src/memory-store.d.ts.map +1 -0
- package/dist/packages/core/src/memory-store.js +2 -0
- package/dist/packages/core/src/memory-store.js.map +1 -0
- package/dist/packages/core/src/memory.d.ts +75 -0
- package/dist/packages/core/src/memory.d.ts.map +1 -0
- package/dist/packages/core/src/memory.js +402 -0
- package/dist/packages/core/src/memory.js.map +1 -0
- package/dist/packages/core/src/policy.d.ts +17 -0
- package/dist/packages/core/src/policy.d.ts.map +1 -0
- package/dist/packages/core/src/policy.js +22 -0
- package/dist/packages/core/src/policy.js.map +1 -0
- package/dist/packages/core/src/types.d.ts +102 -0
- package/dist/packages/core/src/types.d.ts.map +1 -0
- package/dist/packages/core/src/types.js +23 -0
- package/dist/packages/core/src/types.js.map +1 -0
- package/dist/packages/embeddings/src/index.d.ts +4 -0
- package/dist/packages/embeddings/src/index.d.ts.map +1 -0
- package/dist/packages/embeddings/src/index.js +4 -0
- package/dist/packages/embeddings/src/index.js.map +1 -0
- package/dist/packages/embeddings/src/local.d.ts +15 -0
- package/dist/packages/embeddings/src/local.d.ts.map +1 -0
- package/dist/packages/embeddings/src/local.js +45 -0
- package/dist/packages/embeddings/src/local.js.map +1 -0
- package/dist/packages/embeddings/src/openai.d.ts +17 -0
- package/dist/packages/embeddings/src/openai.d.ts.map +1 -0
- package/dist/packages/embeddings/src/openai.js +41 -0
- package/dist/packages/embeddings/src/openai.js.map +1 -0
- package/dist/packages/embeddings/src/provider.d.ts +8 -0
- package/dist/packages/embeddings/src/provider.d.ts.map +1 -0
- package/dist/packages/embeddings/src/provider.js +12 -0
- package/dist/packages/embeddings/src/provider.js.map +1 -0
- package/dist/packages/integrations/src/anthropic.d.ts +15 -0
- package/dist/packages/integrations/src/anthropic.d.ts.map +1 -0
- package/dist/packages/integrations/src/anthropic.js +25 -0
- package/dist/packages/integrations/src/anthropic.js.map +1 -0
- package/dist/packages/integrations/src/index.d.ts +4 -0
- package/dist/packages/integrations/src/index.d.ts.map +1 -0
- package/dist/packages/integrations/src/index.js +4 -0
- package/dist/packages/integrations/src/index.js.map +1 -0
- package/dist/packages/integrations/src/openai.d.ts +27 -0
- package/dist/packages/integrations/src/openai.d.ts.map +1 -0
- package/dist/packages/integrations/src/openai.js +26 -0
- package/dist/packages/integrations/src/openai.js.map +1 -0
- package/dist/packages/integrations/src/vercel.d.ts +24 -0
- package/dist/packages/integrations/src/vercel.d.ts.map +1 -0
- package/dist/packages/integrations/src/vercel.js +20 -0
- package/dist/packages/integrations/src/vercel.js.map +1 -0
- package/dist/packages/retrieval/src/index.d.ts +3 -0
- package/dist/packages/retrieval/src/index.d.ts.map +1 -0
- package/dist/packages/retrieval/src/index.js +3 -0
- package/dist/packages/retrieval/src/index.js.map +1 -0
- package/dist/packages/retrieval/src/ranker.d.ts +21 -0
- package/dist/packages/retrieval/src/ranker.d.ts.map +1 -0
- package/dist/packages/retrieval/src/ranker.js +54 -0
- package/dist/packages/retrieval/src/ranker.js.map +1 -0
- package/dist/packages/retrieval/src/token.d.ts +3 -0
- package/dist/packages/retrieval/src/token.d.ts.map +1 -0
- package/dist/packages/retrieval/src/token.js +7 -0
- package/dist/packages/retrieval/src/token.js.map +1 -0
- package/dist/packages/storage/src/config.d.ts +14 -0
- package/dist/packages/storage/src/config.d.ts.map +1 -0
- package/dist/packages/storage/src/config.js +41 -0
- package/dist/packages/storage/src/config.js.map +1 -0
- package/dist/packages/storage/src/index.d.ts +4 -0
- package/dist/packages/storage/src/index.d.ts.map +1 -0
- package/dist/packages/storage/src/index.js +4 -0
- package/dist/packages/storage/src/index.js.map +1 -0
- package/dist/packages/storage/src/memory-store.d.ts +37 -0
- package/dist/packages/storage/src/memory-store.d.ts.map +1 -0
- package/dist/packages/storage/src/memory-store.js +107 -0
- package/dist/packages/storage/src/memory-store.js.map +1 -0
- package/dist/packages/storage/src/migrations.d.ts +12 -0
- package/dist/packages/storage/src/migrations.d.ts.map +1 -0
- package/dist/packages/storage/src/migrations.js +89 -0
- package/dist/packages/storage/src/migrations.js.map +1 -0
- package/dist/packages/storage/src/postgres-store.d.ts +45 -0
- package/dist/packages/storage/src/postgres-store.d.ts.map +1 -0
- package/dist/packages/storage/src/postgres-store.js +282 -0
- package/dist/packages/storage/src/postgres-store.js.map +1 -0
- package/dist/packages/storage/src/sqlite-store.d.ts +36 -0
- package/dist/packages/storage/src/sqlite-store.d.ts.map +1 -0
- package/dist/packages/storage/src/sqlite-store.js +243 -0
- package/dist/packages/storage/src/sqlite-store.js.map +1 -0
- package/examples/advanced.ts +46 -0
- package/examples/basic.ts +25 -0
- package/package.json +52 -0
- package/packages/core/package.json +13 -0
- package/packages/core/src/consolidation.ts +104 -0
- package/packages/core/src/events.ts +25 -0
- package/packages/core/src/extraction.ts +81 -0
- package/packages/core/src/index.ts +22 -0
- package/packages/core/src/memory-store.ts +50 -0
- package/packages/core/src/memory.ts +447 -0
- package/packages/core/src/policy.ts +36 -0
- package/packages/core/src/types.ts +119 -0
- package/packages/core/tsconfig.json +6 -0
- package/packages/embeddings/package.json +9 -0
- package/packages/embeddings/src/index.ts +3 -0
- package/packages/embeddings/src/local.ts +47 -0
- package/packages/embeddings/src/openai.ts +50 -0
- package/packages/embeddings/src/provider.d.ts.map +1 -0
- package/packages/embeddings/src/provider.js.map +1 -0
- package/packages/embeddings/src/provider.ts +17 -0
- package/packages/embeddings/tsconfig.json +6 -0
- package/packages/integrations/package.json +7 -0
- package/packages/integrations/src/anthropic.ts +34 -0
- package/packages/integrations/src/index.ts +3 -0
- package/packages/integrations/src/openai.ts +52 -0
- package/packages/integrations/src/vercel.ts +26 -0
- package/packages/retrieval/package.json +8 -0
- package/packages/retrieval/src/index.ts +2 -0
- package/packages/retrieval/src/ranker.d.ts.map +1 -0
- package/packages/retrieval/src/ranker.js.map +1 -0
- package/packages/retrieval/src/ranker.ts +77 -0
- package/packages/retrieval/src/token.d.ts.map +1 -0
- package/packages/retrieval/src/token.js.map +1 -0
- package/packages/retrieval/src/token.ts +5 -0
- package/packages/retrieval/tsconfig.json +6 -0
- package/packages/storage/package.json +11 -0
- package/packages/storage/src/config.ts +44 -0
- package/packages/storage/src/index.ts +3 -0
- package/packages/storage/src/memory-store.ts +100 -0
- package/packages/storage/src/migrations.ts +96 -0
- package/packages/storage/src/postgres-store.ts +299 -0
- package/packages/storage/src/sqlite-store.ts +298 -0
- package/packages/storage/tsconfig.json +6 -0
- package/test-mvp.mjs +82 -0
- package/tests/consolidation.test.ts +45 -0
- package/tests/eval.test.ts +102 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
CreateMemoryInputSchema,
|
|
4
|
+
type CreateMemoryInput,
|
|
5
|
+
type ContextOptions,
|
|
6
|
+
type ContextResult,
|
|
7
|
+
type Memory as MemoryRecord,
|
|
8
|
+
type MemoryType,
|
|
9
|
+
type SearchResult,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
import type { MemoryStore } from "./memory-store.js";
|
|
12
|
+
import type { EmbeddingProvider } from "../../embeddings/src/provider.js";
|
|
13
|
+
import { HybridRanker } from "../../retrieval/src/ranker.js";
|
|
14
|
+
import { estimateTokens } from "../../retrieval/src/token.js";
|
|
15
|
+
import { MemoryEvents } from "./events.js";
|
|
16
|
+
import { resolvePolicy, isSensitive, type MemoryPolicy } from "./policy.js";
|
|
17
|
+
import { Consolidator } from "./consolidation.js";
|
|
18
|
+
import type { ExtractorProvider } from "./extraction.js";
|
|
19
|
+
import { cosineSimilarity } from "../../embeddings/src/provider.js";
|
|
20
|
+
|
|
21
|
+
export interface MemoryCreateOptions {
|
|
22
|
+
project: string; // name or id
|
|
23
|
+
store?: MemoryStore;
|
|
24
|
+
embeddingProvider?: EmbeddingProvider;
|
|
25
|
+
extractor?: ExtractorProvider;
|
|
26
|
+
policy?: MemoryPolicy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type MemoryLifecycle = "extracted" | "candidate" | "validated" | "stored" | "retrieved" | "updated" | "archived";
|
|
30
|
+
|
|
31
|
+
export class MemoryEngine {
|
|
32
|
+
private store: MemoryStore;
|
|
33
|
+
private projectId: string;
|
|
34
|
+
private projectName: string;
|
|
35
|
+
private embeddingProvider?: EmbeddingProvider;
|
|
36
|
+
private extractor?: ExtractorProvider;
|
|
37
|
+
private policy: ReturnType<typeof resolvePolicy>;
|
|
38
|
+
public readonly events = new MemoryEvents();
|
|
39
|
+
private ranker: HybridRanker;
|
|
40
|
+
private consolidator: Consolidator;
|
|
41
|
+
|
|
42
|
+
private constructor(opts: { store: MemoryStore; projectId: string; projectName: string; embeddingProvider?: EmbeddingProvider; extractor?: ExtractorProvider; policy?: MemoryPolicy; ranker?: HybridRanker }) {
|
|
43
|
+
this.store = opts.store;
|
|
44
|
+
this.projectId = opts.projectId;
|
|
45
|
+
this.projectName = opts.projectName;
|
|
46
|
+
this.embeddingProvider = opts.embeddingProvider;
|
|
47
|
+
this.extractor = opts.extractor;
|
|
48
|
+
this.policy = resolvePolicy(opts.policy);
|
|
49
|
+
this.ranker = opts.ranker ?? new HybridRanker();
|
|
50
|
+
this.consolidator = new Consolidator(this.store, this.embeddingProvider);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static async create(opts: MemoryCreateOptions): Promise<MemoryEngine> {
|
|
54
|
+
const store: MemoryStore = (opts.store as MemoryStore) ?? (await import("../../storage/src/config.js").then(m => m.createStoreFromConfig(m.resolveStorageConfig())));
|
|
55
|
+
let project = await store.getProjectByName(opts.project);
|
|
56
|
+
if (!project) {
|
|
57
|
+
project = await store.getProject(opts.project);
|
|
58
|
+
}
|
|
59
|
+
if (!project) {
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const p = { id: randomUUID(), name: opts.project, createdAt: now, updatedAt: now };
|
|
62
|
+
await store.createProject(p);
|
|
63
|
+
project = p;
|
|
64
|
+
}
|
|
65
|
+
return new MemoryEngine({
|
|
66
|
+
store,
|
|
67
|
+
projectId: project.id,
|
|
68
|
+
projectName: project.name,
|
|
69
|
+
embeddingProvider: opts.embeddingProvider,
|
|
70
|
+
extractor: opts.extractor,
|
|
71
|
+
policy: opts.policy,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get project() {
|
|
76
|
+
return { id: this.projectId, name: this.projectName };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------- Ingest / Remember ----------
|
|
80
|
+
|
|
81
|
+
on<K extends Parameters<MemoryEvents["on"]>[0]>(event: K, listener: Parameters<MemoryEvents["on"]>[1]): this {
|
|
82
|
+
this.events.on(event as any, listener as any);
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async remember(input: Partial<CreateMemoryInput> & { content: string; type?: MemoryType }): Promise<MemoryRecord> {
|
|
87
|
+
const parsed = CreateMemoryInputSchema.parse(input);
|
|
88
|
+
// Policy checks — minimumImportance applies only to auto-extracted memories; manual remember always allowed
|
|
89
|
+
if (!this.policy.storeSensitiveData && isSensitive(parsed.content)) {
|
|
90
|
+
throw new Error("Refusing to store sensitive data (policy.storeSensitiveData=false)");
|
|
91
|
+
}
|
|
92
|
+
if (this.policy.shouldRemember) {
|
|
93
|
+
const allowed = await this.policy.shouldRemember({
|
|
94
|
+
projectId: this.projectId,
|
|
95
|
+
type: parsed.type,
|
|
96
|
+
content: parsed.content,
|
|
97
|
+
importance: parsed.importance,
|
|
98
|
+
confidence: parsed.confidence,
|
|
99
|
+
status: "active",
|
|
100
|
+
createdAt: new Date(),
|
|
101
|
+
updatedAt: new Date(),
|
|
102
|
+
accessCount: 0,
|
|
103
|
+
metadata: parsed.metadata,
|
|
104
|
+
archived: false,
|
|
105
|
+
} as any);
|
|
106
|
+
if (!allowed) throw new Error("Policy shouldRemember returned false");
|
|
107
|
+
}
|
|
108
|
+
const count = (await this.store.listMemories(this.projectId)).filter((m) => !m.archived).length;
|
|
109
|
+
if (count >= this.policy.maxMemoriesPerProject) {
|
|
110
|
+
throw new Error(`maxMemoriesPerProject ${this.policy.maxMemoriesPerProject} reached`);
|
|
111
|
+
}
|
|
112
|
+
const now = new Date();
|
|
113
|
+
const mem: MemoryRecord = {
|
|
114
|
+
id: randomUUID(),
|
|
115
|
+
projectId: this.projectId,
|
|
116
|
+
type: parsed.type,
|
|
117
|
+
content: parsed.content,
|
|
118
|
+
importance: parsed.importance,
|
|
119
|
+
confidence: parsed.confidence,
|
|
120
|
+
status: "active",
|
|
121
|
+
createdAt: now,
|
|
122
|
+
updatedAt: now,
|
|
123
|
+
accessCount: 0,
|
|
124
|
+
sourceId: parsed.sourceId,
|
|
125
|
+
metadata: parsed.metadata,
|
|
126
|
+
archived: false,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Deduplication: keyword similarity check against existing memories
|
|
130
|
+
const existing = await this.store.listMemories(this.projectId);
|
|
131
|
+
for (const e of existing) {
|
|
132
|
+
if (e.archived || e.status === "superseded") continue;
|
|
133
|
+
if (this.isNearDuplicate(e.content, mem.content)) {
|
|
134
|
+
// update existing instead of creating new
|
|
135
|
+
await this.store.updateMemory(e.id, {
|
|
136
|
+
content: mem.content,
|
|
137
|
+
importance: Math.max(e.importance, mem.importance),
|
|
138
|
+
updatedAt: now,
|
|
139
|
+
accessCount: e.accessCount,
|
|
140
|
+
});
|
|
141
|
+
if (this.embeddingProvider) {
|
|
142
|
+
const vec = await this.embeddingProvider.embed(mem.content);
|
|
143
|
+
await this.store.upsertEmbedding(e.id, vec, this.embeddingProvider.model);
|
|
144
|
+
}
|
|
145
|
+
const updated = await this.store.getMemory(e.id);
|
|
146
|
+
return updated!;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Conflict detection (simple): if embedding similarity high but content contradicts via keyword overlap low? fallback to string includes
|
|
151
|
+
// For MVP: if we detect "switched to" / "migrated" / "instead of", supersede previous conflicting memory
|
|
152
|
+
await this.maybeSupersedeConflicting(mem);
|
|
153
|
+
|
|
154
|
+
await this.store.createMemory(mem);
|
|
155
|
+
|
|
156
|
+
if (this.embeddingProvider) {
|
|
157
|
+
const vec = await this.embeddingProvider.embed(mem.content);
|
|
158
|
+
await this.store.upsertEmbedding(mem.id, vec, this.embeddingProvider.model);
|
|
159
|
+
mem.embedding = vec;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.events.emit("memory.created", mem);
|
|
163
|
+
return mem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** ingest raw conversation — stores source + auto-extracts if policy/extractor configured */
|
|
167
|
+
async ingest(input: { role: string; content: string; sourceType?: "conversation" | "document" | "tool" | "manual" }): Promise<{ sourceId: string; extracted?: MemoryRecord[] }> {
|
|
168
|
+
const sourceId = randomUUID();
|
|
169
|
+
await this.store.createSource({
|
|
170
|
+
id: sourceId,
|
|
171
|
+
projectId: this.projectId,
|
|
172
|
+
type: input.sourceType ?? "conversation",
|
|
173
|
+
content: `${input.role}: ${input.content}`,
|
|
174
|
+
createdAt: new Date(),
|
|
175
|
+
metadata: { role: input.role },
|
|
176
|
+
});
|
|
177
|
+
if (this.policy.autoExtract && this.extractor) {
|
|
178
|
+
const extracted = await this.extractor.extract(input.content);
|
|
179
|
+
const memories: MemoryRecord[] = [];
|
|
180
|
+
for (const e of extracted) {
|
|
181
|
+
if (e.importance < this.policy.minimumImportance) continue;
|
|
182
|
+
try {
|
|
183
|
+
const m = await this.remember({ type: e.type, content: e.content, importance: e.importance, confidence: e.confidence, sourceId, metadata: { autoExtracted: true } });
|
|
184
|
+
memories.push(m);
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
if (!this.policy.retainConversations) {
|
|
188
|
+
// optionally delete source after extraction — keep for traceability if needed
|
|
189
|
+
}
|
|
190
|
+
return { sourceId, extracted: memories };
|
|
191
|
+
}
|
|
192
|
+
return { sourceId };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- Search ----------
|
|
196
|
+
|
|
197
|
+
async search(query: string, opts?: { limit?: number; types?: MemoryType[]; minImportance?: number }): Promise<SearchResult[]> {
|
|
198
|
+
const limit = opts?.limit ?? 5;
|
|
199
|
+
|
|
200
|
+
// If we have embeddings, do semantic + keyword hybrid; else keyword fallback via store.search
|
|
201
|
+
if (this.embeddingProvider) {
|
|
202
|
+
const qVec = await this.embeddingProvider.embed(query);
|
|
203
|
+
|
|
204
|
+
// Prefer native pgvector search if available (faster, uses HNSW index)
|
|
205
|
+
if (this.store.vectorSearch) {
|
|
206
|
+
try {
|
|
207
|
+
const vectorResults = await this.store.vectorSearch(this.projectId, qVec, limit * 3);
|
|
208
|
+
const withVectors = vectorResults
|
|
209
|
+
.filter(({ memory }) => !memory.archived && memory.status !== "archived")
|
|
210
|
+
.filter(({ memory }) => !opts?.types || opts.types.includes(memory.type))
|
|
211
|
+
.filter(({ memory }) => opts?.minImportance === undefined || memory.importance >= opts.minImportance)
|
|
212
|
+
.map(({ memory, distance }) => ({ ...memory, _vec: undefined as any, _distance: distance }));
|
|
213
|
+
// Hybrid rank still applies keyword+importance+recency on top of vector distance
|
|
214
|
+
const candidates = this.ranker.rank(withVectors as any, query, qVec);
|
|
215
|
+
const top = candidates.slice(0, limit);
|
|
216
|
+
for (const c of top) {
|
|
217
|
+
await this.store.updateMemory(c.memory.id, { lastAccessedAt: new Date(), accessCount: c.memory.accessCount + 1 });
|
|
218
|
+
}
|
|
219
|
+
const eventId = randomUUID();
|
|
220
|
+
const scores: Record<string, number> = {};
|
|
221
|
+
top.forEach((r) => (scores[r.memory.id] = r.score));
|
|
222
|
+
const retrievalEvent = { id: eventId, projectId: this.projectId, query, retrievedMemoryIds: top.map((r) => r.memory.id), scores, finalContextTokens: estimateTokens(top.map((r) => r.memory.content).join("\n")), createdAt: new Date() };
|
|
223
|
+
await this.store.logRetrieval(retrievalEvent);
|
|
224
|
+
this.events.emit("retrieval.completed", retrievalEvent);
|
|
225
|
+
this.events.emit("memory.retrieved", top.map((r) => r.memory) as any, query);
|
|
226
|
+
return top.map((r) => ({ memory: r.memory, score: r.score, semanticScore: r.semanticScore, keywordScore: r.keywordScore }));
|
|
227
|
+
} catch {
|
|
228
|
+
// fallback to JS ranking
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const all = await this.store.listMemories(this.projectId);
|
|
233
|
+
const withEmbeddings: Array<MemoryRecord & { _vec?: number[] }> = [];
|
|
234
|
+
for (const m of all) {
|
|
235
|
+
if (m.archived || m.status === "archived") continue;
|
|
236
|
+
if (opts?.types && !opts.types.includes(m.type)) continue;
|
|
237
|
+
if (opts?.minImportance !== undefined && m.importance < opts.minImportance) continue;
|
|
238
|
+
const emb = await this.store.getEmbedding(m.id);
|
|
239
|
+
withEmbeddings.push({ ...m, _vec: emb?.vector });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// rank via HybridRanker (semantic cosine + keyword + importance + recency)
|
|
243
|
+
const candidates = this.ranker.rank(withEmbeddings as any, query, qVec);
|
|
244
|
+
const top = candidates.slice(0, limit);
|
|
245
|
+
|
|
246
|
+
// update access counts
|
|
247
|
+
for (const c of top) {
|
|
248
|
+
await this.store.updateMemory(c.memory.id, {
|
|
249
|
+
lastAccessedAt: new Date(),
|
|
250
|
+
accessCount: c.memory.accessCount + 1,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// log retrieval
|
|
255
|
+
const eventId = randomUUID();
|
|
256
|
+
const scores: Record<string, number> = {};
|
|
257
|
+
top.forEach((r) => (scores[r.memory.id] = r.score));
|
|
258
|
+
const retrievalEvent = {
|
|
259
|
+
id: eventId,
|
|
260
|
+
projectId: this.projectId,
|
|
261
|
+
query,
|
|
262
|
+
retrievedMemoryIds: top.map((r) => r.memory.id),
|
|
263
|
+
scores,
|
|
264
|
+
finalContextTokens: estimateTokens(top.map((r) => r.memory.content).join("\n")),
|
|
265
|
+
createdAt: new Date(),
|
|
266
|
+
};
|
|
267
|
+
await this.store.logRetrieval(retrievalEvent);
|
|
268
|
+
this.events.emit("retrieval.completed", retrievalEvent);
|
|
269
|
+
this.events.emit("memory.retrieved", top.map((r) => r.memory) as any, query);
|
|
270
|
+
|
|
271
|
+
return top.map((r) => ({
|
|
272
|
+
memory: r.memory,
|
|
273
|
+
score: r.score,
|
|
274
|
+
semanticScore: r.semanticScore,
|
|
275
|
+
keywordScore: r.keywordScore,
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// fallback: store's keyword search
|
|
280
|
+
const results = await this.store.search({
|
|
281
|
+
projectId: this.projectId,
|
|
282
|
+
query,
|
|
283
|
+
types: opts?.types,
|
|
284
|
+
limit,
|
|
285
|
+
minImportance: opts?.minImportance,
|
|
286
|
+
});
|
|
287
|
+
for (const r of results) {
|
|
288
|
+
await this.store.updateMemory(r.memory.id, {
|
|
289
|
+
lastAccessedAt: new Date(),
|
|
290
|
+
accessCount: r.memory.accessCount + 1,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return results;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------- Context ----------
|
|
297
|
+
|
|
298
|
+
async context(opts: ContextOptions): Promise<ContextResult>;
|
|
299
|
+
async context(query: string): Promise<ContextResult>;
|
|
300
|
+
async context(opts: string | ContextOptions): Promise<ContextResult> {
|
|
301
|
+
const q = typeof opts === "string" ? opts : opts.query;
|
|
302
|
+
const maxTokens = typeof opts === "string" ? 6000 : (opts.maxTokens ?? 6000);
|
|
303
|
+
const limit = typeof opts === "string" ? 5 : (opts.limit ?? 10);
|
|
304
|
+
const types = typeof opts === "string" ? undefined : opts.types;
|
|
305
|
+
|
|
306
|
+
const results = await this.search(q, { limit, types });
|
|
307
|
+
// token budgeting: greedily add until maxTokens
|
|
308
|
+
const memories: MemoryRecord[] = [];
|
|
309
|
+
let tokenCount = 0;
|
|
310
|
+
const header = `Relevant project memory (${this.projectName}):\n`;
|
|
311
|
+
tokenCount += estimateTokens(header);
|
|
312
|
+
let text = header;
|
|
313
|
+
|
|
314
|
+
for (const r of results) {
|
|
315
|
+
const chunk = `- ${r.memory.content}\n`;
|
|
316
|
+
const chunkTokens = estimateTokens(chunk);
|
|
317
|
+
if (tokenCount + chunkTokens > maxTokens) break;
|
|
318
|
+
memories.push(r.memory as any);
|
|
319
|
+
text += chunk;
|
|
320
|
+
tokenCount += chunkTokens;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (memories.length === 0) {
|
|
324
|
+
text = "";
|
|
325
|
+
tokenCount = 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
memories,
|
|
330
|
+
text,
|
|
331
|
+
tokenCount,
|
|
332
|
+
sources: [...new Set(memories.map((m: any) => m.sourceId).filter(Boolean))] as string[],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// alias
|
|
337
|
+
buildContext(opts: ContextOptions) {
|
|
338
|
+
return this.context(opts);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------- Lifecycle ----------
|
|
342
|
+
|
|
343
|
+
async forget(id: string): Promise<void> {
|
|
344
|
+
const mem = await this.store.getMemory(id);
|
|
345
|
+
await this.store.deleteMemory(id);
|
|
346
|
+
// Embeddings contain derived data — ensure deleted (store cascade handles it, but explicit for InMemory)
|
|
347
|
+
try { await (this.store as any).deleteEmbedding?.(id); } catch {}
|
|
348
|
+
this.events.emit("memory.deleted", id);
|
|
349
|
+
if (mem) this.events.emit("memory.archived", mem);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async forgetProject(projectId?: string): Promise<void> {
|
|
353
|
+
const pid = projectId ?? this.projectId;
|
|
354
|
+
const memories = await this.store.listMemories(pid);
|
|
355
|
+
for (const m of memories) await this.store.deleteMemory(m.id);
|
|
356
|
+
await this.store.deleteProject(pid);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async archive(id: string): Promise<void> {
|
|
360
|
+
await this.store.archiveMemory(id);
|
|
361
|
+
const mem = await this.store.getMemory(id);
|
|
362
|
+
if (mem) this.events.emit("memory.archived", mem);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async deleteProject(): Promise<void> {
|
|
366
|
+
await this.forgetProject(this.projectId);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async export(filter?: { includeArchived?: boolean }): Promise<{ project: { id: string; name: string }; memories: MemoryRecord[] }> {
|
|
370
|
+
let memories = await this.store.listMemories(this.projectId);
|
|
371
|
+
if (!filter?.includeArchived) memories = memories.filter((m) => !m.archived);
|
|
372
|
+
return { project: { id: this.projectId, name: this.projectName }, memories };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async consolidate(opts?: { threshold?: number; minClusterSize?: number }) {
|
|
376
|
+
const results = await this.consolidator.consolidate(this.projectId, opts);
|
|
377
|
+
for (const r of results) this.events.emit("consolidation.completed", r.summary, r.sourceIds);
|
|
378
|
+
return results;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async updateMemory(id: string, update: Partial<MemoryRecord>): Promise<void> {
|
|
382
|
+
await this.store.updateMemory(id, update);
|
|
383
|
+
const mem = await this.store.getMemory(id);
|
|
384
|
+
if (mem) this.events.emit("memory.updated", mem);
|
|
385
|
+
if (update.content && this.embeddingProvider && mem) {
|
|
386
|
+
const vec = await this.embeddingProvider.embed(mem.content);
|
|
387
|
+
await this.store.upsertEmbedding(id, vec, this.embeddingProvider.model);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---------- Helpers ----------
|
|
392
|
+
|
|
393
|
+
private isNearDuplicate(a: string, b: string): boolean {
|
|
394
|
+
const norm = (s: string) => s.toLowerCase().replace(/\W+/g, " ").trim();
|
|
395
|
+
const na = norm(a), nb = norm(b);
|
|
396
|
+
if (na === nb) return true;
|
|
397
|
+
// Jaccard over tokens threshold 0.85
|
|
398
|
+
const sa = new Set(na.split(" "));
|
|
399
|
+
const sb = new Set(nb.split(" "));
|
|
400
|
+
const inter = [...sa].filter((x) => sb.has(x)).length;
|
|
401
|
+
const union = new Set([...sa, ...sb]).size;
|
|
402
|
+
return union > 0 && inter / union > 0.85;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private async maybeSupersedeConflicting(newMem: MemoryRecord): Promise<void> {
|
|
406
|
+
const triggers = ["switched to", "migrated to", "changed to", "instead of", "no longer", "replaced", "now using", "moved to"];
|
|
407
|
+
const lower = newMem.content.toLowerCase();
|
|
408
|
+
const triggered = triggers.some((t) => lower.includes(t));
|
|
409
|
+
const existing = await this.store.listMemories(this.projectId);
|
|
410
|
+
const newTokens = new Set(lower.split(/\W+/).filter(Boolean));
|
|
411
|
+
|
|
412
|
+
// Embedding-aware conflict: if semantic similarity high but content contradicts
|
|
413
|
+
let newVec: number[] | null = null;
|
|
414
|
+
if (this.embeddingProvider) {
|
|
415
|
+
try { newVec = await this.embeddingProvider.embed(newMem.content); } catch {}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (const e of existing) {
|
|
419
|
+
if (e.archived || e.status !== "active") continue;
|
|
420
|
+
const overlap = e.content.toLowerCase().split(/\W+/).filter((w) => newTokens.has(w)).length;
|
|
421
|
+
let shouldSupersede = false;
|
|
422
|
+
|
|
423
|
+
if (triggered && overlap >= 2 && !this.isNearDuplicate(e.content, newMem.content)) {
|
|
424
|
+
const nouns = ["database", "postgres", "mongodb", "sqlite", "storage", "backend", "auth", "payment", "api", "framework"];
|
|
425
|
+
const sharesNoun = nouns.some((n) => e.content.toLowerCase().includes(n) && lower.includes(n));
|
|
426
|
+
if (sharesNoun) shouldSupersede = true;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Embedding similarity conflict: high similarity (>0.82) but not duplicate -> likely same topic updated
|
|
430
|
+
if (!shouldSupersede && newVec && triggered) {
|
|
431
|
+
const emb = await this.store.getEmbedding(e.id);
|
|
432
|
+
if (emb) {
|
|
433
|
+
const sim = cosineSimilarity(newVec, emb.vector);
|
|
434
|
+
if (sim > 0.82 && !this.isNearDuplicate(e.content, newMem.content)) shouldSupersede = true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (shouldSupersede) {
|
|
439
|
+
await this.store.updateMemory(e.id, { status: "superseded", archived: false, updatedAt: new Date() });
|
|
440
|
+
this.events.emit("memory.superseded", e, newMem);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Convenience alias per spec: `Memory` class
|
|
447
|
+
export const Memory = MemoryEngine;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Memory } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface MemoryPolicy {
|
|
4
|
+
autoExtract?: boolean;
|
|
5
|
+
minimumImportance?: number; // default 0.65
|
|
6
|
+
maxMemoriesPerProject?: number; // default 10000
|
|
7
|
+
retainConversations?: boolean; // default false
|
|
8
|
+
storeSensitiveData?: boolean; // default false
|
|
9
|
+
shouldRemember?: (memory: Omit<Memory, "id" | "createdAt" | "updatedAt">) => boolean | Promise<boolean>;
|
|
10
|
+
maxTokensPerContext?: number; // default 6000
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_POLICY: Required<Omit<MemoryPolicy, "shouldRemember">> & { shouldRemember?: MemoryPolicy["shouldRemember"] } = {
|
|
14
|
+
autoExtract: false,
|
|
15
|
+
minimumImportance: 0.65,
|
|
16
|
+
maxMemoriesPerProject: 10000,
|
|
17
|
+
retainConversations: false,
|
|
18
|
+
storeSensitiveData: false,
|
|
19
|
+
maxTokensPerContext: 6000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function resolvePolicy(overrides?: MemoryPolicy): typeof DEFAULT_POLICY {
|
|
23
|
+
return { ...DEFAULT_POLICY, ...overrides };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const SENSITIVE_PATTERNS = [
|
|
27
|
+
/password/i,
|
|
28
|
+
/api[_-]?key/i,
|
|
29
|
+
/secret/i,
|
|
30
|
+
/\bssn\b/i,
|
|
31
|
+
/credit[_-]?card/i,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function isSensitive(content: string): boolean {
|
|
35
|
+
return SENSITIVE_PATTERNS.some((re) => re.test(content));
|
|
36
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// ---------- Memory types ----------
|
|
4
|
+
|
|
5
|
+
export const MemoryTypeSchema = z.enum([
|
|
6
|
+
"fact",
|
|
7
|
+
"preference",
|
|
8
|
+
"decision",
|
|
9
|
+
"event",
|
|
10
|
+
"knowledge",
|
|
11
|
+
"procedure",
|
|
12
|
+
"task",
|
|
13
|
+
"summary",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export type MemoryType = z.infer<typeof MemoryTypeSchema>;
|
|
17
|
+
|
|
18
|
+
export const MemoryStatusSchema = z.enum(["active", "superseded", "archived"]);
|
|
19
|
+
export type MemoryStatus = z.infer<typeof MemoryStatusSchema>;
|
|
20
|
+
|
|
21
|
+
export interface Memory {
|
|
22
|
+
id: string;
|
|
23
|
+
projectId: string;
|
|
24
|
+
type: MemoryType;
|
|
25
|
+
content: string;
|
|
26
|
+
embedding?: number[];
|
|
27
|
+
importance: number; // 0-1
|
|
28
|
+
confidence: number; // 0-1
|
|
29
|
+
status: MemoryStatus;
|
|
30
|
+
createdAt: Date;
|
|
31
|
+
updatedAt: Date;
|
|
32
|
+
lastAccessedAt?: Date;
|
|
33
|
+
accessCount: number;
|
|
34
|
+
sourceId?: string;
|
|
35
|
+
metadata: Record<string, unknown>;
|
|
36
|
+
archived: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Project {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
createdAt: Date;
|
|
44
|
+
updatedAt: Date;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type SourceType = "conversation" | "document" | "tool" | "manual";
|
|
48
|
+
|
|
49
|
+
export interface Source {
|
|
50
|
+
id: string;
|
|
51
|
+
projectId: string;
|
|
52
|
+
type: SourceType;
|
|
53
|
+
content?: string;
|
|
54
|
+
createdAt: Date;
|
|
55
|
+
metadata: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RetrievalEvent {
|
|
59
|
+
id: string;
|
|
60
|
+
projectId: string;
|
|
61
|
+
query: string;
|
|
62
|
+
retrievedMemoryIds: string[];
|
|
63
|
+
scores: Record<string, number>;
|
|
64
|
+
finalContextTokens: number;
|
|
65
|
+
createdAt: Date;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------- Search ----------
|
|
69
|
+
|
|
70
|
+
export interface SearchQuery {
|
|
71
|
+
projectId: string;
|
|
72
|
+
query: string;
|
|
73
|
+
types?: MemoryType[];
|
|
74
|
+
limit?: number;
|
|
75
|
+
minImportance?: number;
|
|
76
|
+
includeArchived?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CandidateMemory extends Memory {
|
|
80
|
+
semanticScore: number;
|
|
81
|
+
keywordScore: number;
|
|
82
|
+
finalScore: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SearchResult {
|
|
86
|
+
memory: Memory;
|
|
87
|
+
score: number;
|
|
88
|
+
semanticScore: number;
|
|
89
|
+
keywordScore: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------- Context ----------
|
|
93
|
+
|
|
94
|
+
export interface ContextOptions {
|
|
95
|
+
query: string;
|
|
96
|
+
maxTokens?: number; // default 6000
|
|
97
|
+
limit?: number;
|
|
98
|
+
types?: MemoryType[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ContextResult {
|
|
102
|
+
memories: Memory[];
|
|
103
|
+
text: string;
|
|
104
|
+
tokenCount: number;
|
|
105
|
+
sources: string[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------- Validation schemas ----------
|
|
109
|
+
|
|
110
|
+
export const CreateMemoryInputSchema = z.object({
|
|
111
|
+
type: MemoryTypeSchema.default("fact"),
|
|
112
|
+
content: z.string().min(1).max(10000),
|
|
113
|
+
importance: z.number().min(0).max(1).default(0.5),
|
|
114
|
+
confidence: z.number().min(0).max(1).default(1),
|
|
115
|
+
sourceId: z.string().optional(),
|
|
116
|
+
metadata: z.record(z.unknown()).default({}),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export type CreateMemoryInput = z.infer<typeof CreateMemoryInputSchema>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EmbeddingProvider } from "./provider.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic, zero-dependency local embedding for tests / offline dev.
|
|
5
|
+
* Hashing trick: char n-grams -> fixed dimensions, L2-normalized.
|
|
6
|
+
* NOT suitable for production quality but ensures storage contract works without API keys.
|
|
7
|
+
*/
|
|
8
|
+
export class LocalEmbeddingProvider implements EmbeddingProvider {
|
|
9
|
+
readonly model = "local-hash-v1";
|
|
10
|
+
readonly dimensions: number;
|
|
11
|
+
|
|
12
|
+
constructor(dimensions = 256) {
|
|
13
|
+
this.dimensions = dimensions;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async embed(text: string): Promise<number[]> {
|
|
17
|
+
return this.hashEmbed(text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
21
|
+
return texts.map((t) => this.hashEmbed(t));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private hashEmbed(text: string): number[] {
|
|
25
|
+
const vec = new Array(this.dimensions).fill(0);
|
|
26
|
+
const normalized = text.toLowerCase();
|
|
27
|
+
// char trigrams
|
|
28
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
29
|
+
const trigram = normalized.slice(i, i + 3);
|
|
30
|
+
if (!trigram) continue;
|
|
31
|
+
let hash = 0;
|
|
32
|
+
for (let j = 0; j < trigram.length; j++) hash = (hash * 31 + trigram.charCodeAt(j)) >>> 0;
|
|
33
|
+
const idx = hash % this.dimensions;
|
|
34
|
+
vec[idx] += 1;
|
|
35
|
+
}
|
|
36
|
+
// token-level boost
|
|
37
|
+
for (const tok of normalized.split(/\W+/).filter(Boolean)) {
|
|
38
|
+
let hash = 0;
|
|
39
|
+
for (let j = 0; j < tok.length; j++) hash = (hash * 131 + tok.charCodeAt(j)) >>> 0;
|
|
40
|
+
const idx = (hash * 7) % this.dimensions;
|
|
41
|
+
vec[idx] += 2;
|
|
42
|
+
}
|
|
43
|
+
// L2 normalize
|
|
44
|
+
const norm = Math.sqrt(vec.reduce((s, v) => s + v * v, 0)) || 1;
|
|
45
|
+
return vec.map((v) => v / norm);
|
|
46
|
+
}
|
|
47
|
+
}
|