@agent-diaries/core 0.1.2 → 0.1.4
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 +24 -1
- package/dist/adapters/mongo.d.ts +2 -0
- package/dist/adapters/mongo.js +20 -1
- package/dist/adapters/redis.js +9 -3
- package/dist/diary.d.ts +3 -0
- package/dist/diary.js +9 -3
- package/package.json +23 -9
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@agent-diaries/core)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](#-200-agent-real-world-cloud-benchmarks)
|
|
7
8
|
[](https://opensource.org/licenses/MIT)
|
|
8
9
|
</div>
|
|
9
10
|
|
|
@@ -126,7 +127,7 @@ console.log(` Actual Locks: ${successful}`); // Always exactly 1.
|
|
|
126
127
|
```
|
|
127
128
|
|
|
128
129
|
### The Results (Zero Race Conditions)
|
|
129
|
-
> *Tested via WAN connection to an Upstash Serverless Redis instance*
|
|
130
|
+
> *Tested via WAN connection to an Upstash Serverless Redis instance and a Free Tier MongoDB Atlas Cluster*
|
|
130
131
|
|
|
131
132
|
```text
|
|
132
133
|
=================================
|
|
@@ -149,6 +150,27 @@ console.log(` Actual Locks: ${successful}`); // Always exactly 1.
|
|
|
149
150
|
Actual Written: 200
|
|
150
151
|
Write Duration: 16267ms
|
|
151
152
|
🟢 PASSED (Zero data corruption)
|
|
153
|
+
|
|
154
|
+
=================================
|
|
155
|
+
🌪️ INITIALIZING 200-AGENT SWARM: MongoDB Atlas (Cloud Free Tier)
|
|
156
|
+
=================================
|
|
157
|
+
[Test 1] The Herd Effect: 200 Agents competing for exactly ONE viral task...
|
|
158
|
+
Expected Locks: 1
|
|
159
|
+
Actual Locks: 1
|
|
160
|
+
Resolution Time: 7362ms
|
|
161
|
+
🟢 PASSED (199 race conditions prevented)
|
|
162
|
+
|
|
163
|
+
[Test 2] Real World Distribution: 200 Agents processing 10 common data tasks...
|
|
164
|
+
Expected Locks: 10
|
|
165
|
+
Actual Locks: 10
|
|
166
|
+
Resolution Time: 5545ms
|
|
167
|
+
🟢 PASSED (190 duplicate LLM calls prevented)
|
|
168
|
+
|
|
169
|
+
[Test 3] Extreme Write Contention: 200 Agents blasting state updates at the exact same time...
|
|
170
|
+
Expected Written: 200
|
|
171
|
+
Actual Written: 200
|
|
172
|
+
Write Duration: 9410ms
|
|
173
|
+
🟢 PASSED (Zero data corruption)
|
|
152
174
|
```
|
|
153
175
|
|
|
154
176
|
**💡 Engineering Insight:** While SQL databases perform well on local network environments, relational connection poolers (like pgBouncer or Supavisor) completely buckle under the massive concurrent TCP bursts generated by serverless AI swarms. **Redis or MongoDB (via atomic upserts)** are strictly required for reliable lock management in high-concurrency serverless edge environments.
|
|
@@ -161,6 +183,7 @@ console.log(` Actual Locks: ${successful}`); // Always exactly 1.
|
|
|
161
183
|
Retrieves the exact string output/result from a previously completed task so your agent can instantly reuse it.
|
|
162
184
|
- **`diary.filterNewTasks(tasks: T[]): Promise<T[]>`**
|
|
163
185
|
Pass in an array of task objects. Returns only the tasks that the agent has *not* seen yet.
|
|
186
|
+
*⚠️ WARNING: This method returns a non-atomic snapshot. Always follow up with `claimTask()` on individual items before acting on them in a high-concurrency environment.*
|
|
164
187
|
- **`diary.writeTaskResult(title: string, result: string): Promise<void>`**
|
|
165
188
|
Saves the final result into the agent's memory bank after the agent finishes its work.
|
|
166
189
|
|
package/dist/adapters/mongo.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { StorageAdapter } from '../storage';
|
|
|
2
2
|
import { Collection } from 'mongodb';
|
|
3
3
|
export declare class MongoStorage<T> implements StorageAdapter<T> {
|
|
4
4
|
private collection;
|
|
5
|
+
private initialized;
|
|
5
6
|
constructor(config: {
|
|
6
7
|
collection: Collection;
|
|
7
8
|
});
|
|
9
|
+
private ensureIndex;
|
|
8
10
|
private hashString;
|
|
9
11
|
get(key: string): Promise<T | null>;
|
|
10
12
|
set(key: string, data: T): Promise<void>;
|
package/dist/adapters/mongo.js
CHANGED
|
@@ -7,9 +7,21 @@ exports.MongoStorage = void 0;
|
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
8
|
class MongoStorage {
|
|
9
9
|
collection;
|
|
10
|
+
initialized = false;
|
|
10
11
|
constructor(config) {
|
|
11
12
|
this.collection = config.collection;
|
|
12
13
|
}
|
|
14
|
+
async ensureIndex() {
|
|
15
|
+
if (this.initialized)
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
await this.collection.createIndex({ lockedAt: 1 }, { expireAfterSeconds: 30, partialFilterExpression: { lockedAt: { $exists: true } } });
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
console.warn('[MongoStorage] Failed to create TTL index:', e);
|
|
22
|
+
}
|
|
23
|
+
this.initialized = true;
|
|
24
|
+
}
|
|
13
25
|
hashString(str) {
|
|
14
26
|
return crypto_1.default.createHash('sha256').update(str).digest('hex');
|
|
15
27
|
}
|
|
@@ -25,6 +37,7 @@ class MongoStorage {
|
|
|
25
37
|
await this.collection.updateOne({ _id: hash }, { $set: { data: JSON.stringify(data) } }, { upsert: true });
|
|
26
38
|
}
|
|
27
39
|
async withLock(key, fn) {
|
|
40
|
+
await this.ensureIndex();
|
|
28
41
|
const hash = this.hashString(key);
|
|
29
42
|
const lockId = `lock:${hash}`;
|
|
30
43
|
const acquireLock = async () => {
|
|
@@ -38,8 +51,14 @@ class MongoStorage {
|
|
|
38
51
|
throw error;
|
|
39
52
|
}
|
|
40
53
|
};
|
|
54
|
+
let attempt = 0;
|
|
41
55
|
while (!(await acquireLock())) {
|
|
42
|
-
|
|
56
|
+
const backoff = Math.min(10 * Math.pow(2, attempt), 500);
|
|
57
|
+
const jitter = Math.random() * 50;
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, backoff + jitter));
|
|
59
|
+
attempt++;
|
|
60
|
+
if (attempt > 60)
|
|
61
|
+
throw new Error(`[MongoStorage] Lock timeout on key: ${key}`);
|
|
43
62
|
}
|
|
44
63
|
try {
|
|
45
64
|
return await fn();
|
package/dist/adapters/redis.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RedisStorage = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
4
5
|
class RedisStorage {
|
|
5
6
|
redis;
|
|
6
7
|
prefix;
|
|
@@ -29,15 +30,20 @@ class RedisStorage {
|
|
|
29
30
|
}
|
|
30
31
|
async withLock(key, fn) {
|
|
31
32
|
const lockKey = `${this.getKey(key)}:lock`;
|
|
32
|
-
const lockValue =
|
|
33
|
+
const lockValue = (0, crypto_1.randomUUID)();
|
|
33
34
|
const lockTtlMs = 10000; // 10 seconds max lock
|
|
34
35
|
const acquireLock = async () => {
|
|
35
36
|
const result = await this.redis.set(lockKey, lockValue, 'PX', lockTtlMs, 'NX');
|
|
36
37
|
return result === 'OK';
|
|
37
38
|
};
|
|
38
|
-
|
|
39
|
+
let attempt = 0;
|
|
39
40
|
while (!(await acquireLock())) {
|
|
40
|
-
|
|
41
|
+
const backoff = Math.min(10 * Math.pow(2, attempt), 500);
|
|
42
|
+
const jitter = Math.random() * 50;
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, backoff + jitter));
|
|
44
|
+
attempt++;
|
|
45
|
+
if (attempt > 60)
|
|
46
|
+
throw new Error(`[RedisStorage] Lock timeout on key: ${key}`);
|
|
41
47
|
}
|
|
42
48
|
try {
|
|
43
49
|
return await fn();
|
package/dist/diary.d.ts
CHANGED
|
@@ -45,6 +45,9 @@ export declare class AgentDiary {
|
|
|
45
45
|
getTaskResult(title: string): Promise<string | undefined>;
|
|
46
46
|
/**
|
|
47
47
|
* Filters out items that the agent has already processed.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ WARNING: filterNewTasks() is a non-atomic snapshot. Always follow it with claimTask()
|
|
50
|
+
* to atomically claim ownership. Never act on filterNewTasks() results directly without claiming them first.
|
|
48
51
|
*/
|
|
49
52
|
filterNewTasks<T extends {
|
|
50
53
|
title: string;
|
package/dist/diary.js
CHANGED
|
@@ -41,7 +41,8 @@ class AgentDiary {
|
|
|
41
41
|
const signature = AgentDiary.normalizeSignature(title);
|
|
42
42
|
return await this.storage.withLock(`diary_${this.agentId}`, async () => {
|
|
43
43
|
const state = await this.readDiary();
|
|
44
|
-
|
|
44
|
+
const seenSet = new Set(state.seenSignatures);
|
|
45
|
+
if (seenSet.has(signature)) {
|
|
45
46
|
return false; // Task already exists
|
|
46
47
|
}
|
|
47
48
|
// Claim it immediately to prevent race conditions
|
|
@@ -65,7 +66,8 @@ class AgentDiary {
|
|
|
65
66
|
async hasProcessedTask(title) {
|
|
66
67
|
const signature = AgentDiary.normalizeSignature(title);
|
|
67
68
|
const state = await this.readDiary();
|
|
68
|
-
|
|
69
|
+
const seenSet = new Set(state.seenSignatures);
|
|
70
|
+
return seenSet.has(signature);
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
71
73
|
* Retrieves the stored result of a previously processed task, if available.
|
|
@@ -78,12 +80,16 @@ class AgentDiary {
|
|
|
78
80
|
}
|
|
79
81
|
/**
|
|
80
82
|
* Filters out items that the agent has already processed.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ WARNING: filterNewTasks() is a non-atomic snapshot. Always follow it with claimTask()
|
|
85
|
+
* to atomically claim ownership. Never act on filterNewTasks() results directly without claiming them first.
|
|
81
86
|
*/
|
|
82
87
|
async filterNewTasks(tasks) {
|
|
83
88
|
const state = await this.readDiary();
|
|
89
|
+
const seenSet = new Set(state.seenSignatures);
|
|
84
90
|
return tasks.filter(task => {
|
|
85
91
|
const signature = AgentDiary.normalizeSignature(task.title);
|
|
86
|
-
return !
|
|
92
|
+
return !seenSet.has(signature);
|
|
87
93
|
});
|
|
88
94
|
}
|
|
89
95
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-diaries/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
|
@@ -14,11 +14,15 @@
|
|
|
14
14
|
"prepublishOnly": "npm run build"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
|
-
"ai",
|
|
18
|
-
"agent",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
17
|
+
"ai-agent",
|
|
18
|
+
"agent-memory",
|
|
19
|
+
"deduplication",
|
|
20
|
+
"distributed-lock",
|
|
21
|
+
"redis",
|
|
22
|
+
"mongodb",
|
|
23
|
+
"serverless",
|
|
24
|
+
"idempotency",
|
|
25
|
+
"task-queue"
|
|
22
26
|
],
|
|
23
27
|
"author": "swapwarick_n",
|
|
24
28
|
"repository": {
|
|
@@ -31,6 +35,7 @@
|
|
|
31
35
|
"homepage": "https://github.com/swapwarick/agent-diaries-core#readme",
|
|
32
36
|
"license": "MIT",
|
|
33
37
|
"devDependencies": {
|
|
38
|
+
"dotenv": "^17.4.2",
|
|
34
39
|
"@types/dotenv": "^6.1.1",
|
|
35
40
|
"@types/node": "^20.0.0",
|
|
36
41
|
"@types/proper-lockfile": "^4.1.4",
|
|
@@ -40,9 +45,18 @@
|
|
|
40
45
|
"vitest": "^4.1.5"
|
|
41
46
|
},
|
|
42
47
|
"dependencies": {
|
|
43
|
-
"dotenv": "^17.4.2",
|
|
44
|
-
"ioredis": "^5.10.1",
|
|
45
|
-
"mongodb": "^7.2.0",
|
|
46
48
|
"proper-lockfile": "^4.1.2"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"ioredis": ">=5",
|
|
52
|
+
"mongodb": ">=6"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"ioredis": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"mongodb": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
47
61
|
}
|
|
48
62
|
}
|