@agent-diaries/core 0.1.41 → 1.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/README.md +44 -9
- package/dist/adapters/mongo.d.ts +6 -4
- package/dist/adapters/mongo.js +9 -7
- package/dist/adapters/sqlite.d.ts +16 -0
- package/dist/adapters/sqlite.js +126 -0
- package/dist/diary.js +2 -7
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -29,11 +29,12 @@ Install the core package:
|
|
|
29
29
|
npm install @agent-diaries/core
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
If you plan to use a specific
|
|
32
|
+
If you plan to use a specific storage adapter, install its peer dependency:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npm install
|
|
36
|
-
npm install
|
|
35
|
+
npm install better-sqlite3 # For SQLite Storage
|
|
36
|
+
npm install ioredis # For Redis Storage
|
|
37
|
+
npm install mongodb # For MongoDB Storage
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
## 🚀 Quick Start
|
|
@@ -99,11 +100,26 @@ const updatedResult = "Found 0 warnings, ALL critical errors resolved.";
|
|
|
99
100
|
await diary.writeTaskResult(currentTask, updatedResult);
|
|
100
101
|
```
|
|
101
102
|
|
|
102
|
-
##
|
|
103
|
+
## 🗄️ Storage Adapters (Cloud & Local Databases)
|
|
103
104
|
|
|
104
|
-
Local file storage is great for local development, but serverless environments (Vercel, AWS Lambda) have ephemeral filesystems
|
|
105
|
+
Local file storage is great for local development, but serverless environments (Vercel, AWS Lambda) have ephemeral filesystems and require lock-safe cloud adapters, while local tools and desktops benefit from relational SQLite coordination.
|
|
105
106
|
|
|
106
|
-
###
|
|
107
|
+
### SQLite (Best for Desktop / Local Apps)
|
|
108
|
+
The `SqliteStorage` adapter uses a local SQLite database (`better-sqlite3`) with atomic UNIQUE constraint insertions and transactional TTL locks for highly reliable multi-process coordination.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { AgentDiary } from '@agent-diaries/core';
|
|
112
|
+
import { SqliteStorage } from '@agent-diaries/core/dist/adapters/sqlite';
|
|
113
|
+
import Database from 'better-sqlite3';
|
|
114
|
+
|
|
115
|
+
const db = new Database('diary.db');
|
|
116
|
+
const diary = new AgentDiary({
|
|
117
|
+
agentId: 'sqlite-bot',
|
|
118
|
+
storage: new SqliteStorage({ db })
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Redis (Best for Serverless / Swarms)
|
|
107
123
|
The `RedisStorage` adapter uses atomic `SETNX` distributed spin-locks to guarantee race-condition safety across thousands of concurrent Vercel Edge functions.
|
|
108
124
|
|
|
109
125
|
```typescript
|
|
@@ -135,13 +151,32 @@ const diary = new AgentDiary({
|
|
|
135
151
|
});
|
|
136
152
|
```
|
|
137
153
|
|
|
138
|
-
## 📊
|
|
154
|
+
## 📊 Enterprise Concurrency Benchmarks
|
|
139
155
|
|
|
140
156
|
Agent Diaries Core is mathematically proven to handle massive concurrent agent swarms without race conditions or database corruption.
|
|
141
157
|
|
|
142
|
-
|
|
158
|
+
### 1. Multi-Process OS-Level Concurrency (Worker Threads)
|
|
159
|
+
To verify true operating-system level process isolation, we spawned 50 independent Node.js `worker_threads` to aggressively hit the cloud databases at the exact same millisecond.
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
🌪️ Spawning 50 Multi-Process Workers for REDIS...
|
|
163
|
+
Expected Locks: 1
|
|
164
|
+
Actual Locks: 1
|
|
165
|
+
Resolution Time: ~4300ms
|
|
166
|
+
🟢 PASSED (49 race conditions prevented across OS processes)
|
|
167
|
+
|
|
168
|
+
🌪️ Spawning 50 Multi-Process Workers for MONGO...
|
|
169
|
+
Expected Locks: 1
|
|
170
|
+
Actual Locks: 1
|
|
171
|
+
Resolution Time: 8192ms
|
|
172
|
+
🟢 PASSED (49 race conditions prevented across OS processes)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 2. 200-Agent Real-World Cloud Scale
|
|
176
|
+
|
|
177
|
+
To prove its viability for global serverless deployments, we rigorously stress-tested the library against live instances, blasting them with **200 serverless agents** executing distributed lock requests across the internet simultaneously.
|
|
143
178
|
|
|
144
|
-
|
|
179
|
+
#### The Real-Life Architecture
|
|
145
180
|
```typescript
|
|
146
181
|
const NUM_AGENTS = 200;
|
|
147
182
|
let agents = Array.from({ length: NUM_AGENTS }, () => getDiary());
|
package/dist/adapters/mongo.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { StorageAdapter } from '../storage';
|
|
2
2
|
import { Collection } from 'mongodb';
|
|
3
|
+
interface MongoStorageOptions {
|
|
4
|
+
collection: Collection;
|
|
5
|
+
}
|
|
3
6
|
export declare class MongoStorage<T> implements StorageAdapter<T> {
|
|
4
7
|
private collection;
|
|
5
|
-
private
|
|
6
|
-
constructor(
|
|
7
|
-
collection: Collection;
|
|
8
|
-
});
|
|
8
|
+
private static indexedCollections;
|
|
9
|
+
constructor(options: MongoStorageOptions);
|
|
9
10
|
private ensureIndex;
|
|
10
11
|
private hashString;
|
|
11
12
|
get(key: string): Promise<T | null>;
|
|
12
13
|
set(key: string, data: T): Promise<void>;
|
|
13
14
|
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
14
15
|
}
|
|
16
|
+
export {};
|
package/dist/adapters/mongo.js
CHANGED
|
@@ -7,20 +7,21 @@ exports.MongoStorage = void 0;
|
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
8
|
class MongoStorage {
|
|
9
9
|
collection;
|
|
10
|
-
|
|
11
|
-
constructor(
|
|
12
|
-
this.collection =
|
|
10
|
+
static indexedCollections = new Set();
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.collection = options.collection;
|
|
13
13
|
}
|
|
14
14
|
async ensureIndex() {
|
|
15
|
-
|
|
15
|
+
const ns = this.collection.namespace;
|
|
16
|
+
if (MongoStorage.indexedCollections.has(ns))
|
|
16
17
|
return;
|
|
17
18
|
try {
|
|
18
|
-
await this.collection.createIndex({ lockedAt: 1 }, { expireAfterSeconds:
|
|
19
|
+
await this.collection.createIndex({ lockedAt: 1 }, { expireAfterSeconds: 10, partialFilterExpression: { lockedAt: { $exists: true } } });
|
|
19
20
|
}
|
|
20
21
|
catch (e) {
|
|
21
22
|
console.warn('[MongoStorage] Failed to create TTL index:', e);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
MongoStorage.indexedCollections.add(ns);
|
|
24
25
|
}
|
|
25
26
|
hashString(str) {
|
|
26
27
|
return crypto_1.default.createHash('sha256').update(str).digest('hex');
|
|
@@ -57,8 +58,9 @@ class MongoStorage {
|
|
|
57
58
|
const jitter = Math.random() * 50;
|
|
58
59
|
await new Promise(resolve => setTimeout(resolve, backoff + jitter));
|
|
59
60
|
attempt++;
|
|
60
|
-
if (attempt >
|
|
61
|
+
if (attempt > 150) {
|
|
61
62
|
throw new Error(`[MongoStorage] Lock timeout on key: ${key}`);
|
|
63
|
+
}
|
|
62
64
|
}
|
|
63
65
|
try {
|
|
64
66
|
return await fn();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StorageAdapter } from '../storage';
|
|
2
|
+
import type { Database } from 'better-sqlite3';
|
|
3
|
+
export interface SqliteStorageOptions {
|
|
4
|
+
db: Database;
|
|
5
|
+
tableName?: string;
|
|
6
|
+
locksTableName?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class SqliteStorage<T> implements StorageAdapter<T> {
|
|
9
|
+
private db;
|
|
10
|
+
private tableName;
|
|
11
|
+
private locksTableName;
|
|
12
|
+
constructor(options: SqliteStorageOptions);
|
|
13
|
+
get(key: string): Promise<T | null>;
|
|
14
|
+
set(key: string, value: T): Promise<void>;
|
|
15
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SqliteStorage = void 0;
|
|
4
|
+
class SqliteStorage {
|
|
5
|
+
db;
|
|
6
|
+
tableName;
|
|
7
|
+
locksTableName;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
if (!options.db) {
|
|
10
|
+
throw new Error('[SqliteStorage] database instance (db) is required.');
|
|
11
|
+
}
|
|
12
|
+
this.db = options.db;
|
|
13
|
+
this.tableName = options.tableName || 'agent_diaries_storage';
|
|
14
|
+
this.locksTableName = options.locksTableName || 'agent_diaries_locks';
|
|
15
|
+
// Initialize tables synchronously as better-sqlite3 is fully synchronous
|
|
16
|
+
this.db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
18
|
+
key TEXT PRIMARY KEY,
|
|
19
|
+
value TEXT
|
|
20
|
+
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS ${this.locksTableName} (
|
|
22
|
+
key TEXT PRIMARY KEY,
|
|
23
|
+
locked_at INTEGER
|
|
24
|
+
);
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
async get(key) {
|
|
28
|
+
try {
|
|
29
|
+
const row = this.db
|
|
30
|
+
.prepare(`SELECT value FROM ${this.tableName} WHERE key = ?`)
|
|
31
|
+
.get(key);
|
|
32
|
+
if (!row)
|
|
33
|
+
return null;
|
|
34
|
+
return JSON.parse(row.value);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error(`[SqliteStorage] Failed to get key ${key}:`, e);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async set(key, value) {
|
|
42
|
+
try {
|
|
43
|
+
const serialized = JSON.stringify(value);
|
|
44
|
+
this.db
|
|
45
|
+
.prepare(`
|
|
46
|
+
INSERT INTO ${this.tableName} (key, value)
|
|
47
|
+
VALUES (?, ?)
|
|
48
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
49
|
+
`)
|
|
50
|
+
.run(key, serialized);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
console.error(`[SqliteStorage] Failed to set key ${key}:`, e);
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async withLock(key, fn) {
|
|
58
|
+
const lockKey = `lock:${key}`;
|
|
59
|
+
const lockTtlMs = 10000; // 10 seconds max lock
|
|
60
|
+
const acquireLock = () => {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
try {
|
|
63
|
+
this.db
|
|
64
|
+
.prepare(`INSERT INTO ${this.locksTableName} (key, locked_at) VALUES (?, ?)`)
|
|
65
|
+
.run(lockKey, now);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === 'SQLITE_CONSTRAINT_PRIMARYKEY' ||
|
|
70
|
+
error.code === 'SQLITE_CONSTRAINT' ||
|
|
71
|
+
(error.message && error.message.includes('UNIQUE constraint failed'))) {
|
|
72
|
+
// Check if the lock has expired
|
|
73
|
+
const existing = this.db
|
|
74
|
+
.prepare(`SELECT locked_at FROM ${this.locksTableName} WHERE key = ?`)
|
|
75
|
+
.get(lockKey);
|
|
76
|
+
if (existing && now - existing.locked_at > lockTtlMs) {
|
|
77
|
+
// Attempt to clear the expired lock and acquire it
|
|
78
|
+
try {
|
|
79
|
+
// Wrap cleanup and acquire in a transaction for atomicity
|
|
80
|
+
const runCleanupAndAcquire = this.db.transaction(() => {
|
|
81
|
+
this.db
|
|
82
|
+
.prepare(`DELETE FROM ${this.locksTableName} WHERE key = ? AND locked_at = ?`)
|
|
83
|
+
.run(lockKey, existing.locked_at);
|
|
84
|
+
this.db
|
|
85
|
+
.prepare(`INSERT INTO ${this.locksTableName} (key, locked_at) VALUES (?, ?)`)
|
|
86
|
+
.run(lockKey, now);
|
|
87
|
+
});
|
|
88
|
+
runCleanupAndAcquire();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch (retryError) {
|
|
92
|
+
// Conflict during retry, lock is still held or acquired by another concurrent process
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
let attempt = 0;
|
|
102
|
+
while (!acquireLock()) {
|
|
103
|
+
const backoff = Math.min(10 * Math.pow(2, attempt), 500);
|
|
104
|
+
const jitter = Math.random() * 50;
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, backoff + jitter));
|
|
106
|
+
attempt++;
|
|
107
|
+
if (attempt > 60) {
|
|
108
|
+
throw new Error(`[SqliteStorage] Lock timeout on key: ${key}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return await fn();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
try {
|
|
116
|
+
this.db
|
|
117
|
+
.prepare(`DELETE FROM ${this.locksTableName} WHERE key = ?`)
|
|
118
|
+
.run(lockKey);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
console.error(`[SqliteStorage] Failed to release lock on key ${key}:`, e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.SqliteStorage = SqliteStorage;
|
package/dist/diary.js
CHANGED
|
@@ -107,13 +107,8 @@ class AgentDiary {
|
|
|
107
107
|
await this.storage.set(`diary_${this.agentId}`, state);
|
|
108
108
|
}
|
|
109
109
|
else {
|
|
110
|
-
// If not claimed first, we
|
|
111
|
-
|
|
112
|
-
state.history = [record, ...state.history].slice(0, this.maxHistory);
|
|
113
|
-
state.seenSignatures = state.history.map(r => r.signature);
|
|
114
|
-
state.runCount += 1;
|
|
115
|
-
state.lastRun = Date.now();
|
|
116
|
-
await this.storage.set(`diary_${this.agentId}`, state);
|
|
110
|
+
// If not claimed first, we throw a loud error
|
|
111
|
+
throw new Error(`[AgentDiary] Task "${title}" was not claimed. Call claimTask() before writeTaskResult().`);
|
|
117
112
|
}
|
|
118
113
|
});
|
|
119
114
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-diaries/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The lightweight, framework-agnostic memory layer for edge AI agents.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
],
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "vitest run",
|
|
12
|
-
"test:coverage": "vitest run --coverage",
|
|
12
|
+
"test:coverage": "vitest run --coverage.reporter=lcov --coverage.reporter=text",
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"prepublishOnly": "npm run build"
|
|
15
15
|
},
|
|
@@ -35,23 +35,31 @@
|
|
|
35
35
|
"homepage": "https://github.com/swapwarick/agent-diaries-core#readme",
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"
|
|
38
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
39
39
|
"@types/dotenv": "^6.1.1",
|
|
40
40
|
"@types/node": "^20.0.0",
|
|
41
41
|
"@types/proper-lockfile": "^4.1.4",
|
|
42
42
|
"@vitest/coverage-v8": "^4.1.5",
|
|
43
|
+
"dotenv": "^17.4.2",
|
|
44
|
+
"ioredis": "^5.5.0",
|
|
45
|
+
"mongodb": "^6.13.0",
|
|
43
46
|
"ts-node": "^10.9.2",
|
|
44
47
|
"typescript": "^5.0.0",
|
|
45
48
|
"vitest": "^4.1.5"
|
|
46
49
|
},
|
|
47
50
|
"dependencies": {
|
|
51
|
+
"mempalace-agent": "^1.0.0",
|
|
48
52
|
"proper-lockfile": "^4.1.2"
|
|
49
53
|
},
|
|
50
54
|
"peerDependencies": {
|
|
55
|
+
"better-sqlite3": ">=9 || >=10 || >=11",
|
|
51
56
|
"ioredis": ">=5",
|
|
52
57
|
"mongodb": ">=6"
|
|
53
58
|
},
|
|
54
59
|
"peerDependenciesMeta": {
|
|
60
|
+
"better-sqlite3": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
55
63
|
"ioredis": {
|
|
56
64
|
"optional": true
|
|
57
65
|
},
|
|
@@ -59,4 +67,4 @@
|
|
|
59
67
|
"optional": true
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
|
-
}
|
|
70
|
+
}
|