@agent-diaries/core 0.1.3 → 0.1.41
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 +38 -7
- 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
|
@@ -69,6 +69,35 @@ async function runAgent() {
|
|
|
69
69
|
|
|
70
70
|
runAgent();
|
|
71
71
|
```
|
|
72
|
+
### Forcefully Re-running a Task (The Engineering Trick)
|
|
73
|
+
|
|
74
|
+
If you want an agent to strictly avoid duplicate work, use `await diary.claimTask(task)`. It will automatically return `false` if it was done.
|
|
75
|
+
|
|
76
|
+
But if an agent wants to explicitly overwrite or re-do a task because the user demanded it, you skip `claimTask()` entirely and just write the final result using `await diary.writeTaskResult(task, newResult)`. This seamlessly replaces the old memory with the new one.
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
🤖 Agent Alice: Claiming and performing 'Generate Monthly Report'...
|
|
80
|
+
-> Task done! Saving result.
|
|
81
|
+
--------------------------------------------------
|
|
82
|
+
🤖 Agent Bob: Checking if 'Generate Monthly Report' is done...
|
|
83
|
+
-> 🛑 Found in Diary! Previous result: "Report for May: $12,000 Revenue."
|
|
84
|
+
-> 💬 Informs User: "This report was already generated. Do you want me to re-run it with the latest data?"
|
|
85
|
+
-> 👤 User responds: YES
|
|
86
|
+
-> Agent Bob forcefully re-running the task...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// 1. Check if it's already done (for logging/informing user)
|
|
91
|
+
if (await diary.hasProcessedTask(currentTask)) {
|
|
92
|
+
console.log("Task is already done. Forcefully re-running per user request...");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Perform the work again
|
|
96
|
+
const updatedResult = "Found 0 warnings, ALL critical errors resolved.";
|
|
97
|
+
|
|
98
|
+
// 3. Skip claimTask() and directly overwrite the old memory
|
|
99
|
+
await diary.writeTaskResult(currentTask, updatedResult);
|
|
100
|
+
```
|
|
72
101
|
|
|
73
102
|
## ☁️ Cloud Storage Adapters (Production)
|
|
74
103
|
|
|
@@ -88,20 +117,21 @@ const diary = new AgentDiary({
|
|
|
88
117
|
});
|
|
89
118
|
```
|
|
90
119
|
|
|
91
|
-
###
|
|
92
|
-
The `
|
|
120
|
+
### MongoDB (Best for Document Scaling)
|
|
121
|
+
The `MongoStorage` adapter natively uses atomic `_id` unique insertion constraints to guarantee row-level safety during concurrent task evaluation, with built-in TTL lock expiration.
|
|
93
122
|
|
|
94
123
|
```typescript
|
|
95
124
|
import { AgentDiary } from '@agent-diaries/core';
|
|
96
|
-
import {
|
|
97
|
-
import {
|
|
125
|
+
import { MongoStorage } from '@agent-diaries/core/dist/adapters/mongo';
|
|
126
|
+
import { MongoClient } from 'mongodb';
|
|
98
127
|
|
|
99
|
-
const
|
|
100
|
-
await
|
|
128
|
+
const client = new MongoClient(process.env.MONGO_URI);
|
|
129
|
+
await client.connect();
|
|
130
|
+
const collection = client.db('agent_diaries').collection('tasks');
|
|
101
131
|
|
|
102
132
|
const diary = new AgentDiary({
|
|
103
133
|
agentId: 'db-bot',
|
|
104
|
-
storage:
|
|
134
|
+
storage: new MongoStorage({ collection })
|
|
105
135
|
});
|
|
106
136
|
```
|
|
107
137
|
|
|
@@ -183,6 +213,7 @@ console.log(` Actual Locks: ${successful}`); // Always exactly 1.
|
|
|
183
213
|
Retrieves the exact string output/result from a previously completed task so your agent can instantly reuse it.
|
|
184
214
|
- **`diary.filterNewTasks(tasks: T[]): Promise<T[]>`**
|
|
185
215
|
Pass in an array of task objects. Returns only the tasks that the agent has *not* seen yet.
|
|
216
|
+
*⚠️ 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.*
|
|
186
217
|
- **`diary.writeTaskResult(title: string, result: string): Promise<void>`**
|
|
187
218
|
Saves the final result into the agent's memory bank after the agent finishes its work.
|
|
188
219
|
|
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.41",
|
|
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
|
}
|