@1mbrain/core 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/src/types.ts ADDED
@@ -0,0 +1,229 @@
1
+ /**
2
+ * 1MBrain Core Types
3
+ *
4
+ * Central type definitions for the memory engine.
5
+ * These types are the contract between all layers of the system.
6
+ */
7
+
8
+ // ─── Memory Types ────────────────────────────────────────
9
+
10
+ export type MemoryType = 'episodic' | 'semantic' | 'procedural' | 'entity' | 'warning';
11
+
12
+ export type AssociationOrigin = 'co-occurrence' | 'similarity' | 'explicit';
13
+
14
+ export type AssociationRelationType = 'relates_to' | 'supersedes' | 'derived_from';
15
+
16
+ export interface Memory {
17
+ id: string;
18
+ agentId: string;
19
+ type: MemoryType;
20
+ content: string;
21
+ embeddingModel: string | null;
22
+ embedding: number[] | null;
23
+ importance: number;
24
+ decayScore: number;
25
+ createdAt: Date;
26
+ lastAccessedAt: Date;
27
+ tags: string[];
28
+ /** Optional structured metadata — e.g. sourceUrl, confidence, evidence from ingest pipeline. */
29
+ metadata?: Record<string, unknown>;
30
+ }
31
+
32
+ export interface Association {
33
+ sourceId: string;
34
+ targetId: string;
35
+ strength: number;
36
+ origin: AssociationOrigin;
37
+ /** Semantic meaning of the relationship. Defaults to 'relates_to'. */
38
+ relationType: AssociationRelationType;
39
+ createdAt: Date;
40
+ }
41
+
42
+ // ─── API Request/Response Types ─────────────────────────
43
+
44
+ export interface CreateMemoryInput {
45
+ agentId: string;
46
+ type: MemoryType;
47
+ content: string;
48
+ importance?: number;
49
+ tags?: string[];
50
+ /** Optional structured metadata stored alongside the memory (e.g. sourceUrl, evidence, confidence). */
51
+ metadata?: Record<string, unknown>;
52
+ associations?: Array<{
53
+ targetId: string;
54
+ strength?: number;
55
+ relationType?: AssociationRelationType;
56
+ }>;
57
+ }
58
+
59
+ export interface SearchMemoryInput {
60
+ agentId: string;
61
+ query: string;
62
+ type?: MemoryType;
63
+ tags?: string[];
64
+ limit?: number;
65
+ threshold?: number;
66
+ useSpreadingActivation?: boolean;
67
+ maxHops?: number;
68
+ activationThreshold?: number;
69
+ blendWeight?: number;
70
+ }
71
+
72
+ export interface SearchResult {
73
+ memory: Memory;
74
+ score: number;
75
+ source: 'vector' | 'association' | 'combined' | 'lexical';
76
+ rankingTrace?: string[];
77
+ }
78
+
79
+ export interface CreateAssociationInput {
80
+ sourceId: string;
81
+ targetId: string;
82
+ agentId?: string;
83
+ strength?: number;
84
+ origin?: AssociationOrigin;
85
+ relationType?: AssociationRelationType;
86
+ }
87
+
88
+ // ─── Memory Passport Types ──────────────────────────────
89
+
90
+ export interface MemoryPassport {
91
+ version: string;
92
+ exportedAt: Date;
93
+ sourceAgent: string;
94
+ embeddingModel: string;
95
+ memories: Memory[];
96
+ associations: Association[];
97
+ metadata: {
98
+ totalMemories: number;
99
+ totalAssociations: number;
100
+ memoryTypes: Record<MemoryType, number>;
101
+ };
102
+ }
103
+
104
+ export interface MemoryPassportEnvelope {
105
+ format: '1mbrain.passport.envelope';
106
+ version: string;
107
+ exportedAt: string;
108
+ sourceAgent: string;
109
+ compression: 'gzip';
110
+ encoding: 'base64';
111
+ encryption: {
112
+ algorithm: 'aes-256-gcm';
113
+ iv: string;
114
+ authTag: string;
115
+ };
116
+ payload: string;
117
+ }
118
+
119
+ // ─── Embedding Provider Interface ───────────────────────
120
+
121
+ export interface EmbeddingProvider {
122
+ readonly name: string;
123
+ readonly model: string;
124
+ readonly dimensions: number;
125
+ embed(text: string): Promise<number[]>;
126
+ embedBatch(texts: string[]): Promise<number[][]>;
127
+ }
128
+
129
+ // ─── Event Types (for WebSocket/Pub-Sub) ────────────────
130
+
131
+ export type MemoryEventType =
132
+ | 'memory:created'
133
+ | 'memory:accessed'
134
+ | 'memory:updated'
135
+ | 'memory:deleted'
136
+ | 'memory:consolidated'
137
+ | 'association:created'
138
+ | 'association:deleted';
139
+
140
+ export interface MemoryEvent {
141
+ type: MemoryEventType;
142
+ memoryId: string;
143
+ agentId: string;
144
+ memoryType?: MemoryType;
145
+ timestamp: Date;
146
+ data?: Record<string, unknown>;
147
+ }
148
+
149
+ // ─── Database Provider Interface ────────────────────────
150
+
151
+ export interface DatabaseProvider {
152
+ // Memory CRUD
153
+ createMemory(memory: Omit<Memory, 'createdAt' | 'lastAccessedAt'>): Promise<Memory>;
154
+ getMemoryById(id: string, agentId: string): Promise<Memory | null>;
155
+ updateMemory(id: string, agentId: string, updates: Partial<Memory>): Promise<Memory | null>;
156
+ deleteMemory(id: string, agentId: string): Promise<boolean>;
157
+
158
+ // Vector search
159
+ searchByVector(
160
+ agentId: string,
161
+ embedding: number[],
162
+ options: {
163
+ limit?: number;
164
+ threshold?: number;
165
+ type?: MemoryType;
166
+ tags?: string[];
167
+ },
168
+ ): Promise<Array<{ memory: Memory; similarity: number }>>;
169
+
170
+ // Associations
171
+ createAssociation(association: Omit<Association, 'createdAt'>): Promise<Association>;
172
+ getAssociations(memoryId: string): Promise<Association[]>;
173
+ deleteAssociations(memoryId: string): Promise<number>;
174
+
175
+ // Bulk operations
176
+ listAgentIds(): Promise<string[]>;
177
+ getAllMemories(agentId: string): Promise<Memory[]>;
178
+ getAllAssociations(agentId: string): Promise<Association[]>;
179
+ bulkCreateMemories(
180
+ memories: Array<Omit<Memory, 'createdAt' | 'lastAccessedAt'>>,
181
+ ): Promise<Memory[]>;
182
+ bulkCreateAssociations(
183
+ associations: Array<Omit<Association, 'createdAt'>>,
184
+ ): Promise<Association[]>;
185
+
186
+ // Decay
187
+ applyDecay(decayRate: number, minScore: number): Promise<number>;
188
+ applyAssociationDecay(decayRate: number, minStrength: number): Promise<number>;
189
+
190
+ // Lifecycle
191
+ initialize(): Promise<void>;
192
+ close(): Promise<void>;
193
+ }
194
+
195
+ // ─── Config Types ───────────────────────────────────────
196
+
197
+ export interface OneMBrainConfig {
198
+ database: {
199
+ provider: 'sqlite' | 'postgres';
200
+ sqlitePath?: string;
201
+ postgresUrl?: string;
202
+ };
203
+ embedding: {
204
+ provider: 'openai' | 'ollama' | 'claude' | 'local-keyword';
205
+ openai?: {
206
+ apiKey: string;
207
+ model: string;
208
+ };
209
+ ollama?: {
210
+ baseUrl: string;
211
+ model: string;
212
+ };
213
+ claude?: {
214
+ apiKey: string;
215
+ model: string;
216
+ };
217
+ localKeyword?: {
218
+ dimensions?: number;
219
+ };
220
+ };
221
+ redis?: {
222
+ url: string;
223
+ };
224
+ decay?: {
225
+ rate: number;
226
+ intervalMs: number;
227
+ minScore: number;
228
+ };
229
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Recall Accuracy & Latency Benchmark
3
+ *
4
+ * Compares pure vector recall against vector + spreading activation using
5
+ * SQLite in-memory storage. Run with:
6
+ *
7
+ * npm exec --workspace=packages/core tsx tests/benchmark.ts
8
+ */
9
+
10
+ import { performance } from 'node:perf_hooks';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { MemoryEngine } from '../src/engine.js';
13
+ import { SqliteDatabaseProvider } from '../src/db/sqlite-provider.js';
14
+ import { InMemoryEventBus } from '../src/events.js';
15
+ import { logger } from '../src/logger.js';
16
+ import type { EmbeddingProvider, Memory } from '../src/types.js';
17
+
18
+ class KeywordEmbeddingProvider implements EmbeddingProvider {
19
+ readonly name = 'keyword-benchmark';
20
+ readonly model = 'keyword-benchmark-v1';
21
+ readonly dimensions = 12;
22
+
23
+ async embed(text: string): Promise<number[]> {
24
+ const lower = text.toLowerCase();
25
+ const keywords = [
26
+ 'alpha',
27
+ 'beta',
28
+ 'pricing',
29
+ 'language',
30
+ 'typescript',
31
+ 'backup',
32
+ 'drive',
33
+ 'memory',
34
+ 'dashboard',
35
+ 'agent',
36
+ 'procedure',
37
+ 'preference',
38
+ ];
39
+
40
+ return keywords.map((keyword) => (lower.includes(keyword) ? 1 : 0));
41
+ }
42
+
43
+ async embedBatch(texts: string[]): Promise<number[][]> {
44
+ return Promise.all(texts.map((text) => this.embed(text)));
45
+ }
46
+ }
47
+
48
+ async function runBenchmark() {
49
+ logger.level = 'warn';
50
+ console.warn('--- 1MBrain Recall Benchmark ---');
51
+
52
+ const db = new SqliteDatabaseProvider(':memory:');
53
+ await db.initialize();
54
+ const engine = new MemoryEngine(db, new KeywordEmbeddingProvider(), new InMemoryEventBus());
55
+
56
+ try {
57
+ const memories: Memory[] = [];
58
+ for (let i = 0; i < 40; i++) {
59
+ memories.push(
60
+ await engine.remember({
61
+ agentId: 'bench-agent',
62
+ type: i % 3 === 0 ? 'episodic' : i % 3 === 1 ? 'semantic' : 'procedural',
63
+ content: `memory item ${i} ${i % 5 === 0 ? 'alpha pricing' : 'dashboard agent'}`,
64
+ tags: [`bucket-${i % 10}`],
65
+ }),
66
+ );
67
+ }
68
+
69
+ const seed = await engine.remember({
70
+ agentId: 'bench-agent',
71
+ type: 'semantic',
72
+ content: 'alpha pricing policy',
73
+ tags: ['target'],
74
+ });
75
+ const graphOnly = await engine.remember({
76
+ agentId: 'bench-agent',
77
+ type: 'procedural',
78
+ content: 'procedure for renewal escalation',
79
+ tags: ['target'],
80
+ });
81
+
82
+ await engine.associate({
83
+ sourceId: seed.id,
84
+ targetId: graphOnly.id,
85
+ agentId: 'bench-agent',
86
+ strength: 1,
87
+ origin: 'explicit',
88
+ });
89
+
90
+ const vectorStart = performance.now();
91
+ const vectorOnly = await engine.recall({
92
+ agentId: 'bench-agent',
93
+ query: 'alpha pricing',
94
+ limit: 20,
95
+ threshold: 0.75,
96
+ useSpreadingActivation: false,
97
+ });
98
+ const vectorMs = performance.now() - vectorStart;
99
+
100
+ const graphStart = performance.now();
101
+ const spread = await engine.recall({
102
+ agentId: 'bench-agent',
103
+ query: 'alpha pricing',
104
+ limit: 50,
105
+ threshold: 0.75,
106
+ useSpreadingActivation: true,
107
+ activationThreshold: 0.1,
108
+ blendWeight: 0.35,
109
+ });
110
+ const spreadMs = performance.now() - graphStart;
111
+
112
+ console.warn(`Dataset: ${memories.length + 2} memories`);
113
+ console.warn(`Vector-only: ${vectorOnly.length} results in ${vectorMs.toFixed(2)}ms`);
114
+ console.warn(`Spreading activation: ${spread.length} results in ${spreadMs.toFixed(2)}ms`);
115
+ console.warn(
116
+ `Graph-only target surfaced: ${spread.some((result) => result.memory.id === graphOnly.id)}`,
117
+ );
118
+ } finally {
119
+ await engine.shutdown();
120
+ }
121
+ }
122
+
123
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
124
+ runBenchmark().catch(console.error);
125
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Embedding Provider Tests
3
+ *
4
+ * Mocks global fetch to test OpenAI and Ollama adapters without real API calls.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
8
+ import { OpenAIEmbeddingProvider } from '../src/embedding/openai-provider.js';
9
+ import { OllamaEmbeddingProvider } from '../src/embedding/ollama-provider.js';
10
+
11
+ const mockFetch = vi.fn();
12
+ global.fetch = mockFetch;
13
+
14
+ describe('Embedding Providers', () => {
15
+ beforeEach(() => {
16
+ mockFetch.mockReset();
17
+ });
18
+
19
+ describe('OpenAIEmbeddingProvider', () => {
20
+ it('should embed a single text correctly', async () => {
21
+ mockFetch.mockResolvedValueOnce({
22
+ ok: true,
23
+ json: async () => ({
24
+ data: [{ embedding: [0.1, 0.2, 0.3], index: 0 }],
25
+ usage: { prompt_tokens: 5, total_tokens: 5 },
26
+ }),
27
+ });
28
+
29
+ const provider = new OpenAIEmbeddingProvider('fake-key', 'text-embedding-3-small');
30
+ const result = await provider.embed('Hello world');
31
+
32
+ expect(result).toEqual([0.1, 0.2, 0.3]);
33
+ expect(mockFetch).toHaveBeenCalledTimes(1);
34
+
35
+ const callArgs = mockFetch.mock.calls[0];
36
+ expect(callArgs[0]).toBe('https://api.openai.com/v1/embeddings');
37
+ expect(callArgs[1].headers.Authorization).toBe('Bearer fake-key');
38
+ expect(JSON.parse(callArgs[1].body)).toEqual({
39
+ model: 'text-embedding-3-small',
40
+ input: ['Hello world'],
41
+ });
42
+ });
43
+
44
+ it('should embed a batch of texts correctly and preserve order', async () => {
45
+ mockFetch.mockResolvedValueOnce({
46
+ ok: true,
47
+ json: async () => ({
48
+ data: [
49
+ { embedding: [0.4, 0.5], index: 1 },
50
+ { embedding: [0.1, 0.2], index: 0 },
51
+ ],
52
+ }),
53
+ });
54
+
55
+ const provider = new OpenAIEmbeddingProvider('fake-key');
56
+ const result = await provider.embedBatch(['First', 'Second']);
57
+
58
+ expect(result).toEqual([[0.1, 0.2], [0.4, 0.5]]);
59
+ });
60
+
61
+ it('should throw an error on API failure', async () => {
62
+ mockFetch.mockResolvedValueOnce({
63
+ ok: false,
64
+ status: 401,
65
+ text: async () => 'Invalid API Key',
66
+ });
67
+
68
+ const provider = new OpenAIEmbeddingProvider('fake-key');
69
+ await expect(provider.embed('Test')).rejects.toThrow('OpenAI embedding error (401): Invalid API Key');
70
+ });
71
+ });
72
+
73
+ describe('OllamaEmbeddingProvider', () => {
74
+ it('should embed a single text correctly', async () => {
75
+ mockFetch.mockResolvedValueOnce({
76
+ ok: true,
77
+ json: async () => ({
78
+ model: 'nomic-embed-text',
79
+ embeddings: [[0.5, 0.6, 0.7]],
80
+ }),
81
+ });
82
+
83
+ const provider = new OllamaEmbeddingProvider('http://localhost:11434', 'nomic-embed-text');
84
+ const result = await provider.embed('Hello world');
85
+
86
+ expect(result).toEqual([0.5, 0.6, 0.7]);
87
+
88
+ const callArgs = mockFetch.mock.calls[0];
89
+ expect(callArgs[0]).toBe('http://localhost:11434/api/embed');
90
+ expect(JSON.parse(callArgs[1].body)).toEqual({
91
+ model: 'nomic-embed-text',
92
+ input: 'Hello world',
93
+ });
94
+ });
95
+
96
+ it('should handle trailing slashes in base URL', async () => {
97
+ mockFetch.mockResolvedValueOnce({
98
+ ok: true,
99
+ json: async () => ({ embeddings: [[0.1]] }),
100
+ });
101
+
102
+ const provider = new OllamaEmbeddingProvider('http://127.0.0.1:11434/');
103
+ await provider.embed('Test');
104
+
105
+ expect(mockFetch.mock.calls[0][0]).toBe('http://127.0.0.1:11434/api/embed');
106
+ });
107
+
108
+ it('should throw an error on failure', async () => {
109
+ mockFetch.mockResolvedValueOnce({
110
+ ok: false,
111
+ status: 500,
112
+ text: async () => 'Internal Server Error',
113
+ });
114
+
115
+ const provider = new OllamaEmbeddingProvider();
116
+ await expect(provider.embed('Test')).rejects.toThrow('Ollama embedding error (500): Internal Server Error');
117
+ });
118
+ });
119
+ });