@o-lang/agent-memory 1.0.3 → 1.0.5

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/capability.js ADDED
@@ -0,0 +1,209 @@
1
+ // capability.js
2
+ require('dotenv').config();
3
+ const Database = require('better-sqlite3');
4
+ const crypto = require('node:crypto');
5
+
6
+ const dbPath = process.env.SQLITE_PATH || './agent_memory.db';
7
+ const db = new Database(dbPath);
8
+
9
+ // ── TABLE SETUP ─────────────────────────────────────────
10
+ db.exec(`
11
+ CREATE TABLE IF NOT EXISTS chat_turns (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ session_id TEXT NOT NULL,
14
+ role TEXT NOT NULL,
15
+ content TEXT NOT NULL,
16
+ token_hash TEXT NOT NULL,
17
+ created_at INTEGER NOT NULL
18
+ );
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_chat_session
21
+ ON chat_turns (session_id, created_at);
22
+
23
+ CREATE TABLE IF NOT EXISTS facts (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ session_id TEXT NOT NULL,
26
+ role TEXT NOT NULL,
27
+ content TEXT NOT NULL,
28
+ token_hash TEXT NOT NULL,
29
+ score REAL NOT NULL,
30
+ created_at INTEGER NOT NULL
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_facts_session
34
+ ON facts (session_id, score DESC, created_at ASC);
35
+ `);
36
+
37
+ // ── HELPER: HASH CONTENT ───────────────────────────────
38
+ function genTokenHash(sessionId, role, content) {
39
+ return crypto.createHash('sha256')
40
+ .update(`${sessionId}:${role}:${content}:${Date.now()}`)
41
+ .digest('hex')
42
+ .slice(0, 16);
43
+ }
44
+
45
+ // ── CHAT MEMORY FUNCTIONS ─────────────────────────────
46
+ function memoryWrite(sessionId, role, content) {
47
+ if (!sessionId) throw new Error("memory_write requires 'session_id'");
48
+ if (!['user', 'assistant', 'system'].includes(role)) throw new Error(`Invalid role '${role}'`);
49
+ if (!content) throw new Error("memory_write requires 'content'");
50
+
51
+ const tokenHash = genTokenHash(sessionId, role, content);
52
+ const res = db.prepare(`
53
+ INSERT INTO chat_turns (session_id, role, content, token_hash, created_at)
54
+ VALUES (?, ?, ?, ?, ?)
55
+ `).run(sessionId, role, content, tokenHash, Date.now());
56
+
57
+ return { turn_index: res.lastInsertRowid, session_id: sessionId, role, token_hash: tokenHash };
58
+ }
59
+
60
+ function memoryRead(sessionId, lastN = 10) {
61
+ if (!sessionId) throw new Error("memory_read requires 'session_id'");
62
+
63
+ const rows = db.prepare(`
64
+ SELECT role, content, created_at FROM (
65
+ SELECT role, content, created_at
66
+ FROM chat_turns
67
+ WHERE session_id = ?
68
+ ORDER BY created_at DESC
69
+ LIMIT ?
70
+ ) ORDER BY created_at ASC
71
+ `).all(sessionId, lastN);
72
+
73
+ const text = rows.map(r => `${r.role === 'user' ? 'User' : r.role === 'assistant' ? 'Assistant' : 'System'}: ${r.content}`).join('\n');
74
+ return { text, turns: rows, empty: rows.length === 0 };
75
+ }
76
+
77
+ // ── FACT MEMORY FUNCTIONS ─────────────────────────────
78
+ function memoryFactWrite(sessionId, role, content, score = 1.0) {
79
+ if (!sessionId || !content) throw new Error("memory_fact_write requires sessionId and content");
80
+ const tokenHash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
81
+ const existing = db.prepare(`SELECT 1 FROM facts WHERE session_id=? AND token_hash=?`).get(sessionId, tokenHash);
82
+ if (existing) return null; // deduplicate
83
+
84
+ const res = db.prepare(`
85
+ INSERT INTO facts (session_id, role, content, token_hash, score, created_at)
86
+ VALUES (?, ?, ?, ?, ?, ?)
87
+ `).run(sessionId, role, content, tokenHash, score, Date.now());
88
+
89
+ return { fact_id: res.lastInsertRowid, session_id: sessionId, role, token_hash: tokenHash, score };
90
+ }
91
+
92
+ function memoryFactWriteBatch(sessionId, facts = []) {
93
+ if (!Array.isArray(facts)) throw new Error("facts must be an array");
94
+ const results = [];
95
+ const insert = db.prepare(`
96
+ INSERT INTO facts (session_id, role, content, token_hash, score, created_at)
97
+ VALUES (?, ?, ?, ?, ?, ?)
98
+ `);
99
+
100
+ const existingHashes = db.prepare(`SELECT token_hash FROM facts WHERE session_id=?`).all(sessionId).map(r => r.token_hash);
101
+ for (const f of facts) {
102
+ const tokenHash = crypto.createHash('sha256').update(f.content).digest('hex').slice(0, 16);
103
+ if (!existingHashes.includes(tokenHash)) {
104
+ const res = insert.run(sessionId, f.role || 'system', f.content, tokenHash, f.score || 1.0, Date.now());
105
+ results.push({ fact_id: res.lastInsertRowid, session_id: sessionId, token_hash: tokenHash, score: f.score || 1.0 });
106
+ }
107
+ }
108
+ return results;
109
+ }
110
+
111
+ function memoryFactRead(sessionId, limit = 20) {
112
+ const rows = db.prepare(`
113
+ SELECT role, content, token_hash, score
114
+ FROM facts
115
+ WHERE session_id = ?
116
+ ORDER BY score DESC, created_at ASC
117
+ LIMIT ?
118
+ `).all(sessionId, limit);
119
+
120
+ const text = rows.map(r => `${r.role === 'user' ? 'User' : r.role === 'assistant' ? 'Assistant' : 'System'}: ${r.content}`).join('\n');
121
+ return { text, facts: rows, empty: rows.length === 0 };
122
+ }
123
+
124
+ // ── SELF-MAINTAIN MEMORY ─────────────────────────────
125
+ async function selfMaintainMemory(sessionId, llmResolver, opts = {}) {
126
+ const { threshold = 20, factScore = 0.8, lastNTurns = 20 } = opts;
127
+
128
+ const recent = memoryRead(sessionId, lastNTurns);
129
+ if (recent.turns.length < threshold) return null;
130
+
131
+ // Auto summarization using LLM resolver
132
+ let summary = '';
133
+ if (llmResolver) {
134
+ const prompt = `Summarize the following conversation into concise facts:\n${recent.text}`;
135
+ const llmResponse = await llmResolver(prompt);
136
+ summary = llmResponse?.text || recent.turns.map(t => t.content).join(' | ');
137
+ } else {
138
+ summary = recent.turns.map(t => t.content).join(' | ');
139
+ }
140
+
141
+ return memoryFactWrite(sessionId, 'system', summary, factScore);
142
+ }
143
+
144
+ // ── SEMANTIC QUERY / RETRIEVAL ───────────────────────
145
+ function memoryQuery(sessionId, query, limit = 5) {
146
+ // Simple token matching across facts
147
+ const rows = db.prepare(`
148
+ SELECT role, content, token_hash, score
149
+ FROM facts
150
+ WHERE session_id = ? AND content LIKE ?
151
+ ORDER BY score DESC, created_at ASC
152
+ LIMIT ?
153
+ `).all(sessionId, `%${query}%`, limit);
154
+
155
+ return rows;
156
+ }
157
+
158
+ // ── CONTEXT INJECTION ───────────────────────────────
159
+ function memoryContext(sessionId, maxTurns = 10, maxFacts = 5) {
160
+ const recent = memoryRead(sessionId, maxTurns);
161
+ const topFacts = memoryFactRead(sessionId, maxFacts);
162
+
163
+ const context = [
164
+ topFacts.text ? `### Agent Facts:\n${topFacts.text}` : '',
165
+ recent.text ? `### Recent Chat:\n${recent.text}` : ''
166
+ ].filter(Boolean).join('\n\n');
167
+
168
+ return context;
169
+ }
170
+
171
+ // ── COUNT & PRUNE ────────────────────────────────────
172
+ function memoryCount(sessionId) {
173
+ const chatCount = db.prepare(`SELECT COUNT(*) AS count FROM chat_turns WHERE session_id=?`).get(sessionId).count;
174
+ const factCount = db.prepare(`SELECT COUNT(*) AS count FROM facts WHERE session_id=?`).get(sessionId).count;
175
+ return { chatCount, factCount };
176
+ }
177
+
178
+ function memoryPrune(sessionId, keepLastTurns = 20, minFactScore = 0.1) {
179
+ const deletedTurns = db.prepare(`
180
+ DELETE FROM chat_turns
181
+ WHERE id NOT IN (
182
+ SELECT id FROM chat_turns
183
+ WHERE session_id=?
184
+ ORDER BY created_at DESC
185
+ LIMIT ?
186
+ )
187
+ `).run(sessionId, keepLastTurns).changes;
188
+
189
+ const deletedFacts = db.prepare(`
190
+ DELETE FROM facts
191
+ WHERE session_id=? AND score < ?
192
+ `).run(sessionId, minFactScore).changes;
193
+
194
+ return { deletedTurns, deletedFacts };
195
+ }
196
+
197
+ // ── EXPORTS ─────────────────────────────────────────
198
+ module.exports = {
199
+ memoryWrite,
200
+ memoryRead,
201
+ memoryFactWrite,
202
+ memoryFactWriteBatch,
203
+ memoryFactRead,
204
+ selfMaintainMemory,
205
+ memoryQuery,
206
+ memoryContext,
207
+ memoryCount,
208
+ memoryPrune
209
+ };
@@ -0,0 +1,122 @@
1
+ // capability.test.js
2
+ // Tests for @o-lang/agent-memory (v2)
3
+
4
+ let memoryWrite, memoryRead, memoryFactWrite, memoryFactWriteBatch, memoryFactRead;
5
+
6
+ beforeEach(() => {
7
+ jest.resetModules();
8
+ process.env.SQLITE_PATH = ':memory:';
9
+
10
+ const cap = require('./capability');
11
+
12
+ memoryWrite = cap.memoryWrite;
13
+ memoryRead = cap.memoryRead;
14
+ memoryFactWrite = cap.memoryFactWrite;
15
+ memoryFactWriteBatch = cap.memoryFactWriteBatch;
16
+ memoryFactRead = cap.memoryFactRead;
17
+ });
18
+
19
+ // ── CHAT MEMORY ─────────────────────────────────────
20
+
21
+ describe('memoryWrite / memoryRead', () => {
22
+
23
+ test('Writes and reads chat history', () => {
24
+ const sid = 'chat-test';
25
+
26
+ memoryWrite(sid, 'user', 'Hello');
27
+ memoryWrite(sid, 'assistant', 'Hi there');
28
+
29
+ const result = memoryRead(sid, 10);
30
+
31
+ expect(result.empty).toBe(false);
32
+ expect(result.turns.length).toBe(2);
33
+ expect(result.text).toContain('Hello');
34
+ });
35
+
36
+ test('Respects lastN limit', () => {
37
+ const sid = 'limit-test';
38
+
39
+ for (let i = 0; i < 5; i++) {
40
+ memoryWrite(sid, 'user', `Msg ${i}`);
41
+ }
42
+
43
+ const result = memoryRead(sid, 2);
44
+
45
+ expect(result.turns.length).toBe(2);
46
+ expect(result.turns[1].content).toBe('Msg 4');
47
+ });
48
+
49
+ test('Sessions are isolated', () => {
50
+ memoryWrite('a', 'user', 'A');
51
+ memoryWrite('b', 'user', 'B');
52
+
53
+ const a = memoryRead('a');
54
+ const b = memoryRead('b');
55
+
56
+ expect(a.text.includes('B')).toBe(false);
57
+ expect(b.text.includes('A')).toBe(false);
58
+ });
59
+
60
+ });
61
+
62
+ // ── FACT MEMORY ─────────────────────────────────────
63
+
64
+ describe('memoryFactWrite / memoryFactRead', () => {
65
+
66
+ test('Writes and reads facts', () => {
67
+ const sid = 'fact-test';
68
+
69
+ memoryFactWrite(sid, 'system', 'User likes short answers', 0.9);
70
+
71
+ const result = memoryFactRead(sid, 10);
72
+
73
+ expect(result.empty).toBe(false);
74
+ expect(result.facts.length).toBe(1);
75
+ });
76
+
77
+ test('Deduplicates identical facts', () => {
78
+ const sid = 'dedup-test';
79
+
80
+ memoryFactWrite(sid, 'system', 'Same fact', 0.9);
81
+ memoryFactWrite(sid, 'system', 'Same fact', 0.8);
82
+
83
+ const result = memoryFactRead(sid);
84
+
85
+ expect(result.facts.length).toBe(1);
86
+ });
87
+
88
+ });
89
+
90
+ // ── BATCH FACT WRITE ────────────────────────────────
91
+
92
+ describe('memoryFactWriteBatch', () => {
93
+
94
+ test('Writes multiple facts', () => {
95
+ const sid = 'batch-test';
96
+
97
+ const res = memoryFactWriteBatch(sid, [
98
+ { content: 'Fact 1', score: 0.9 },
99
+ { content: 'Fact 2', score: 0.8 }
100
+ ]);
101
+
102
+ expect(res.written).toBe(2);
103
+
104
+ const result = memoryFactRead(sid);
105
+
106
+ expect(result.facts.length).toBe(2);
107
+ });
108
+
109
+ test('Skips duplicates in batch', () => {
110
+ const sid = 'batch-dedup';
111
+
112
+ memoryFactWrite(sid, 'system', 'Fact 1', 0.9);
113
+
114
+ const res = memoryFactWriteBatch(sid, [
115
+ { content: 'Fact 1', score: 0.9 },
116
+ { content: 'Fact 2', score: 0.8 }
117
+ ]);
118
+
119
+ expect(res.written).toBe(1);
120
+ });
121
+
122
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/agent-memory",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Unified agent memory resolver for O-Lang. Combines short-term chat memory and long-term facts with deduplication and scoring.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -29,7 +29,7 @@
29
29
  "arm64"
30
30
  ],
31
31
  "files": [
32
- "index.js",
32
+ "*.js",
33
33
  "README.md",
34
34
  "LICENSE"
35
35
  ],
package/resolver.js ADDED
@@ -0,0 +1,38 @@
1
+ module.exports = {
2
+ resolverName: "agent-memory",
3
+ version: "2.0.0",
4
+ specVersion: "O-Lang/1.1",
5
+
6
+ inputs: [
7
+ {
8
+ name: "action",
9
+ type: "string",
10
+ required: true,
11
+ description: "Action string (memory_write, memory_read, memory_fact_write_batch, etc.)"
12
+ }
13
+ ],
14
+
15
+ outputs: [
16
+ { name: "text", type: "string" },
17
+ { name: "turns", type: "array" },
18
+ { name: "facts", type: "array" },
19
+ { name: "turn_index", type: "number" }
20
+ ],
21
+
22
+ exampleAction: 'memory_read "session-001"',
23
+
24
+ failures: [
25
+ { code: "MISSING_SESSION_ID", retries: 0 },
26
+ { code: "INVALID_ROLE", retries: 0 },
27
+ { code: "MISSING_CONTENT", retries: 0 },
28
+ { code: "INVALID_JSON", retries: 0 },
29
+ { code: "LLM_PARSE_FAILED", retries: 0 },
30
+ { code: "DB_ERROR", retries: 1 }
31
+ ],
32
+
33
+ properties: {
34
+ handlesTemplatedPrompts: false,
35
+ deterministic: false,
36
+ sideEffects: true
37
+ }
38
+ };
@@ -0,0 +1,24 @@
1
+ // test-local-resolver.js
2
+ const resolver = require('./index');
3
+
4
+ async function test() {
5
+ console.log('=== WRITE ===');
6
+ console.log('User Turn:', resolver('Action memory_write "s1" "user" "Hello agent!"'));
7
+ console.log('Assistant Turn:', resolver('Action memory_write "s1" "assistant" "Hello, how can I help?"'));
8
+ console.log('System Fact:', resolver('Action memory_fact_write "s1" "system" "Always answer concisely" "0.9"'));
9
+
10
+ console.log('\n=== READ ===');
11
+ console.log(await resolver('Action memory_read "s1" "10"'));
12
+ console.log(await resolver('Action memory_fact_read "s1" "10"'));
13
+
14
+ console.log('\n=== SELF MAINTAIN ===');
15
+ console.log(await resolver('Action self_maintain "s1" "2" "0.8" "10"'));
16
+
17
+ console.log('\n=== COUNT ===');
18
+ console.log(resolver('Action count "s1"'));
19
+
20
+ console.log('\n=== PRUNE ===');
21
+ console.log(resolver('Action prune "s1" "1" "0.5"'));
22
+ }
23
+
24
+ test();