@agent-diaries/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/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/adapters/postgres.d.ts +17 -0
- package/dist/adapters/postgres.js +83 -0
- package/dist/adapters/redis.d.ts +14 -0
- package/dist/adapters/redis.js +58 -0
- package/dist/diary.d.ts +56 -0
- package/dist/diary.js +115 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/storage.d.ts +18 -0
- package/dist/storage.js +95 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Swapnil Netankar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>🧠 Agent Diaries Core</h1>
|
|
3
|
+
<p><strong>The lightweight, lock-safe memory layer for edge AI agents.</strong></p>
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@agent-diaries/core)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<br />
|
|
11
|
+
|
|
12
|
+
**Agent Diaries** is a framework-agnostic state management library designed specifically for autonomous AI agents. It gives your agents a persistent "diary" memory, allowing them to remember past actions, avoid infinite loops, and share context across highly concurrent swarm deployments.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ✨ Features
|
|
17
|
+
|
|
18
|
+
- **🚫 Deduplication & Loop Prevention:** Automatically filter out tasks your agent has already seen.
|
|
19
|
+
- **🔒 Fully Lock-Safe:** Uses atomic spin-locks and advisory locks to completely eliminate race conditions, even with 50+ concurrent agents processing the exact same task simultaneously.
|
|
20
|
+
- **☁️ Cloud-Native Adapters:** Comes with official adapters for **Redis** and **PostgreSQL** for Vercel/AWS Lambda deployments, plus a local file adapter for development.
|
|
21
|
+
- **⚡ Ultra-Lightweight:** Negligible bundle size, zero heavy dependencies.
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
Install the core package:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @agent-diaries/core
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If you plan to use a specific cloud adapter, install its peer dependency:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install ioredis # For Redis Storage
|
|
35
|
+
npm install pg # For PostgreSQL Storage
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 🚀 Quick Start
|
|
39
|
+
|
|
40
|
+
Initialize an `AgentDiary` and wrap your LLM calls to prevent duplicate executions.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { AgentDiary } from '@agent-diaries/core';
|
|
44
|
+
|
|
45
|
+
async function runAgent() {
|
|
46
|
+
const diary = new AgentDiary({ agentId: 'data-collector' });
|
|
47
|
+
const currentTask = 'Download Q3 Financial Report';
|
|
48
|
+
|
|
49
|
+
// 1. claimTask is ATOMIC. It acquires a distributed lock and registers the task.
|
|
50
|
+
// If two agents try to claim it at the exact same millisecond, only ONE succeeds.
|
|
51
|
+
const isNew = await diary.claimTask(currentTask);
|
|
52
|
+
|
|
53
|
+
if (!isNew) {
|
|
54
|
+
const pastResult = await diary.getTaskResult(currentTask);
|
|
55
|
+
console.log(`[Agent] ⏩ Skipping task. Result: ${pastResult}`);
|
|
56
|
+
return pastResult;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Execute your expensive LLM logic safely
|
|
60
|
+
console.log(`[Agent] ⚙️ Executing: "${currentTask}"...`);
|
|
61
|
+
const result = "Found 2 warnings, no critical errors.";
|
|
62
|
+
|
|
63
|
+
// 3. Update the pending task with the final result
|
|
64
|
+
await diary.writeTaskResult(currentTask, result);
|
|
65
|
+
console.log(`[Agent] ✅ Task complete. Diary updated!`);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
runAgent();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## ☁️ Cloud Storage Adapters (Production)
|
|
73
|
+
|
|
74
|
+
Local file storage is great for local development, but serverless environments (Vercel, AWS Lambda) have ephemeral filesystems. For production, you **must** use one of our lock-safe cloud adapters.
|
|
75
|
+
|
|
76
|
+
### Redis (Best for Serverless)
|
|
77
|
+
The `RedisStorage` adapter uses atomic `SETNX` distributed spin-locks to guarantee race-condition safety across thousands of concurrent Vercel Edge functions.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { AgentDiary } from '@agent-diaries/core';
|
|
81
|
+
import { RedisStorage } from '@agent-diaries/core/dist/adapters/redis';
|
|
82
|
+
import Redis from 'ioredis';
|
|
83
|
+
|
|
84
|
+
const diary = new AgentDiary({
|
|
85
|
+
agentId: 'cloud-bot',
|
|
86
|
+
storage: new RedisStorage({ redis: new Redis(process.env.REDIS_URL) })
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### PostgreSQL (Best for Stateful Architectures)
|
|
91
|
+
The `PostgresStorage` adapter natively uses `pg_advisory_lock` to ensure absolute row-level safety during concurrent task evaluation.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { AgentDiary } from '@agent-diaries/core';
|
|
95
|
+
import { PostgresStorage } from '@agent-diaries/core/dist/adapters/postgres';
|
|
96
|
+
import { Pool } from 'pg';
|
|
97
|
+
|
|
98
|
+
const pgStorage = new PostgresStorage({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) });
|
|
99
|
+
await pgStorage.initialize(); // Creates the state table
|
|
100
|
+
|
|
101
|
+
const diary = new AgentDiary({
|
|
102
|
+
agentId: 'db-bot',
|
|
103
|
+
storage: pgStorage
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 📚 API Reference
|
|
108
|
+
|
|
109
|
+
- **`diary.claimTask(title: string): Promise<boolean>`**
|
|
110
|
+
Atomically checks if a task has been processed. If not, acquires a lock and claims it as 'pending'. Returns `true` if successfully claimed.
|
|
111
|
+
- **`diary.getTaskResult(title: string): Promise<string | undefined>`**
|
|
112
|
+
Retrieves the exact string output/result from a previously completed task so your agent can instantly reuse it.
|
|
113
|
+
- **`diary.filterNewTasks(tasks: T[]): Promise<T[]>`**
|
|
114
|
+
Pass in an array of task objects. Returns only the tasks that the agent has *not* seen yet.
|
|
115
|
+
- **`diary.writeTaskResult(title: string, result: string): Promise<void>`**
|
|
116
|
+
Saves the final result into the agent's memory bank after the agent finishes its work.
|
|
117
|
+
|
|
118
|
+
## 📄 License
|
|
119
|
+
|
|
120
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StorageAdapter } from '../storage';
|
|
2
|
+
import { Pool } from 'pg';
|
|
3
|
+
export declare class PostgresStorage<T> implements StorageAdapter<T> {
|
|
4
|
+
private pool;
|
|
5
|
+
private tableName;
|
|
6
|
+
constructor(options: {
|
|
7
|
+
pool: Pool;
|
|
8
|
+
tableName?: string;
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Initializes the database table. Must be called before first use.
|
|
12
|
+
*/
|
|
13
|
+
initialize(): Promise<void>;
|
|
14
|
+
get(key: string): Promise<T | null>;
|
|
15
|
+
set(key: string, value: T): Promise<void>;
|
|
16
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PostgresStorage = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
class PostgresStorage {
|
|
39
|
+
pool;
|
|
40
|
+
tableName;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.pool = options.pool;
|
|
43
|
+
this.tableName = options.tableName || 'agent_diaries';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Initializes the database table. Must be called before first use.
|
|
47
|
+
*/
|
|
48
|
+
async initialize() {
|
|
49
|
+
await this.pool.query(`
|
|
50
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
51
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
52
|
+
data JSONB NOT NULL
|
|
53
|
+
);
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
async get(key) {
|
|
57
|
+
const res = await this.pool.query(`SELECT data FROM ${this.tableName} WHERE id = $1`, [key]);
|
|
58
|
+
if (res.rows.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
return res.rows[0].data;
|
|
61
|
+
}
|
|
62
|
+
async set(key, value) {
|
|
63
|
+
await this.pool.query(`
|
|
64
|
+
INSERT INTO ${this.tableName} (id, data)
|
|
65
|
+
VALUES ($1, $2)
|
|
66
|
+
ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data;
|
|
67
|
+
`, [key, JSON.stringify(value)]);
|
|
68
|
+
}
|
|
69
|
+
async withLock(key, fn) {
|
|
70
|
+
const client = await this.pool.connect();
|
|
71
|
+
// Convert string key to a 32-bit integer for Postgres advisory locks
|
|
72
|
+
const lockId = crypto.createHash('sha256').update(key).digest().readInt32BE(0);
|
|
73
|
+
try {
|
|
74
|
+
await client.query('SELECT pg_advisory_lock($1)', [lockId]);
|
|
75
|
+
return await fn();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await client.query('SELECT pg_advisory_unlock($1)', [lockId]);
|
|
79
|
+
client.release();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.PostgresStorage = PostgresStorage;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { StorageAdapter } from '../storage';
|
|
2
|
+
import Redis from 'ioredis';
|
|
3
|
+
export declare class RedisStorage<T> implements StorageAdapter<T> {
|
|
4
|
+
private redis;
|
|
5
|
+
private prefix;
|
|
6
|
+
constructor(options: {
|
|
7
|
+
redis: Redis;
|
|
8
|
+
prefix?: string;
|
|
9
|
+
});
|
|
10
|
+
private getKey;
|
|
11
|
+
get(key: string): Promise<T | null>;
|
|
12
|
+
set(key: string, value: T): Promise<void>;
|
|
13
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisStorage = void 0;
|
|
4
|
+
class RedisStorage {
|
|
5
|
+
redis;
|
|
6
|
+
prefix;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.redis = options.redis;
|
|
9
|
+
this.prefix = options.prefix || 'agent-diaries:';
|
|
10
|
+
}
|
|
11
|
+
getKey(key) {
|
|
12
|
+
return `${this.prefix}${key}`;
|
|
13
|
+
}
|
|
14
|
+
async get(key) {
|
|
15
|
+
const data = await this.redis.get(this.getKey(key));
|
|
16
|
+
if (!data)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(data);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
console.error(`[RedisStorage] Failed to parse JSON for key ${key}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async set(key, value) {
|
|
27
|
+
const data = JSON.stringify(value);
|
|
28
|
+
await this.redis.set(this.getKey(key), data);
|
|
29
|
+
}
|
|
30
|
+
async withLock(key, fn) {
|
|
31
|
+
const lockKey = `${this.getKey(key)}:lock`;
|
|
32
|
+
const lockValue = Date.now().toString() + Math.random().toString();
|
|
33
|
+
const lockTtlMs = 10000; // 10 seconds max lock
|
|
34
|
+
const acquireLock = async () => {
|
|
35
|
+
const result = await this.redis.set(lockKey, lockValue, 'PX', lockTtlMs, 'NX');
|
|
36
|
+
return result === 'OK';
|
|
37
|
+
};
|
|
38
|
+
// Spin-lock until we acquire it
|
|
39
|
+
while (!(await acquireLock())) {
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return await fn();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
// Safe release: ensure we only delete the lock if we still own it
|
|
47
|
+
const luaScript = `
|
|
48
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
49
|
+
return redis.call("del", KEYS[1])
|
|
50
|
+
else
|
|
51
|
+
return 0
|
|
52
|
+
end
|
|
53
|
+
`;
|
|
54
|
+
await this.redis.eval(luaScript, 1, lockKey, lockValue);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.RedisStorage = RedisStorage;
|
package/dist/diary.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { StorageAdapter } from './storage';
|
|
2
|
+
export interface TaskRecord {
|
|
3
|
+
title: string;
|
|
4
|
+
signature: string;
|
|
5
|
+
result?: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentState {
|
|
9
|
+
lastRun: number;
|
|
10
|
+
seenSignatures: string[];
|
|
11
|
+
runCount: number;
|
|
12
|
+
history: TaskRecord[];
|
|
13
|
+
}
|
|
14
|
+
export interface AgentDiaryOptions {
|
|
15
|
+
agentId: string;
|
|
16
|
+
storage?: StorageAdapter<AgentState>;
|
|
17
|
+
maxHistory?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class AgentDiary {
|
|
20
|
+
private agentId;
|
|
21
|
+
private storage;
|
|
22
|
+
private maxHistory;
|
|
23
|
+
constructor(options: AgentDiaryOptions);
|
|
24
|
+
private emptyState;
|
|
25
|
+
/**
|
|
26
|
+
* Generates a normalized signature for a task title.
|
|
27
|
+
*/
|
|
28
|
+
static normalizeSignature(title: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Reads the current diary state (without locking).
|
|
31
|
+
*/
|
|
32
|
+
readDiary(): Promise<AgentState>;
|
|
33
|
+
/**
|
|
34
|
+
* Atomically attempts to claim a task.
|
|
35
|
+
* Returns true if successfully claimed (first time seen), false if already claimed/processed.
|
|
36
|
+
*/
|
|
37
|
+
claimTask(title: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a task has already been processed by the agent.
|
|
40
|
+
*/
|
|
41
|
+
hasProcessedTask(title: string): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves the stored result of a previously processed task, if available.
|
|
44
|
+
*/
|
|
45
|
+
getTaskResult(title: string): Promise<string | undefined>;
|
|
46
|
+
/**
|
|
47
|
+
* Filters out items that the agent has already processed.
|
|
48
|
+
*/
|
|
49
|
+
filterNewTasks<T extends {
|
|
50
|
+
title: string;
|
|
51
|
+
}>(tasks: T[]): Promise<T[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Updates a claimed task with its final result.
|
|
54
|
+
*/
|
|
55
|
+
writeTaskResult(title: string, result?: string): Promise<void>;
|
|
56
|
+
}
|
package/dist/diary.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentDiary = void 0;
|
|
4
|
+
const storage_1 = require("./storage");
|
|
5
|
+
class AgentDiary {
|
|
6
|
+
agentId;
|
|
7
|
+
storage;
|
|
8
|
+
maxHistory;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.agentId = options.agentId;
|
|
11
|
+
// Use local file storage by default if none provided
|
|
12
|
+
this.storage = options.storage || new storage_1.LocalFileStorage();
|
|
13
|
+
this.maxHistory = options.maxHistory || 500;
|
|
14
|
+
}
|
|
15
|
+
emptyState() {
|
|
16
|
+
return {
|
|
17
|
+
lastRun: 0,
|
|
18
|
+
seenSignatures: [],
|
|
19
|
+
runCount: 0,
|
|
20
|
+
history: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generates a normalized signature for a task title.
|
|
25
|
+
*/
|
|
26
|
+
static normalizeSignature(title) {
|
|
27
|
+
return (title || '').toLowerCase().trim().replace(/\s+/g, ' ');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reads the current diary state (without locking).
|
|
31
|
+
*/
|
|
32
|
+
async readDiary() {
|
|
33
|
+
const state = await this.storage.get(`diary_${this.agentId}`);
|
|
34
|
+
return state ?? this.emptyState();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Atomically attempts to claim a task.
|
|
38
|
+
* Returns true if successfully claimed (first time seen), false if already claimed/processed.
|
|
39
|
+
*/
|
|
40
|
+
async claimTask(title) {
|
|
41
|
+
const signature = AgentDiary.normalizeSignature(title);
|
|
42
|
+
return await this.storage.withLock(`diary_${this.agentId}`, async () => {
|
|
43
|
+
const state = await this.readDiary();
|
|
44
|
+
if (state.seenSignatures.includes(signature)) {
|
|
45
|
+
return false; // Task already exists
|
|
46
|
+
}
|
|
47
|
+
// Claim it immediately to prevent race conditions
|
|
48
|
+
const record = {
|
|
49
|
+
title,
|
|
50
|
+
signature,
|
|
51
|
+
timestamp: Date.now()
|
|
52
|
+
};
|
|
53
|
+
state.history = [record, ...state.history].slice(0, this.maxHistory);
|
|
54
|
+
// Fix Desync Bug: seenSignatures is strictly derived from the sliced history
|
|
55
|
+
state.seenSignatures = state.history.map(r => r.signature);
|
|
56
|
+
state.runCount += 1;
|
|
57
|
+
state.lastRun = Date.now();
|
|
58
|
+
await this.storage.set(`diary_${this.agentId}`, state);
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Checks if a task has already been processed by the agent.
|
|
64
|
+
*/
|
|
65
|
+
async hasProcessedTask(title) {
|
|
66
|
+
const signature = AgentDiary.normalizeSignature(title);
|
|
67
|
+
const state = await this.readDiary();
|
|
68
|
+
return state.seenSignatures.includes(signature);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Retrieves the stored result of a previously processed task, if available.
|
|
72
|
+
*/
|
|
73
|
+
async getTaskResult(title) {
|
|
74
|
+
const signature = AgentDiary.normalizeSignature(title);
|
|
75
|
+
const state = await this.readDiary();
|
|
76
|
+
const record = state.history.find(r => r.signature === signature);
|
|
77
|
+
return record?.result;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Filters out items that the agent has already processed.
|
|
81
|
+
*/
|
|
82
|
+
async filterNewTasks(tasks) {
|
|
83
|
+
const state = await this.readDiary();
|
|
84
|
+
return tasks.filter(task => {
|
|
85
|
+
const signature = AgentDiary.normalizeSignature(task.title);
|
|
86
|
+
return !state.seenSignatures.includes(signature);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Updates a claimed task with its final result.
|
|
91
|
+
*/
|
|
92
|
+
async writeTaskResult(title, result) {
|
|
93
|
+
const signature = AgentDiary.normalizeSignature(title);
|
|
94
|
+
await this.storage.withLock(`diary_${this.agentId}`, async () => {
|
|
95
|
+
const state = await this.readDiary();
|
|
96
|
+
const recordIndex = state.history.findIndex(r => r.signature === signature);
|
|
97
|
+
if (recordIndex !== -1) {
|
|
98
|
+
state.history[recordIndex].result = result;
|
|
99
|
+
state.history[recordIndex].timestamp = Date.now(); // update timestamp
|
|
100
|
+
state.lastRun = Date.now();
|
|
101
|
+
await this.storage.set(`diary_${this.agentId}`, state);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// If not claimed first, we insert it
|
|
105
|
+
const record = { title, signature, result, timestamp: Date.now() };
|
|
106
|
+
state.history = [record, ...state.history].slice(0, this.maxHistory);
|
|
107
|
+
state.seenSignatures = state.history.map(r => r.signature);
|
|
108
|
+
state.runCount += 1;
|
|
109
|
+
state.lastRun = Date.now();
|
|
110
|
+
await this.storage.set(`diary_${this.agentId}`, state);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.AgentDiary = AgentDiary;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./storage"), exports);
|
|
18
|
+
__exportStar(require("./diary"), exports);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface StorageAdapter<T> {
|
|
2
|
+
get(key: string): Promise<T | null>;
|
|
3
|
+
set(key: string, value: T): Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Acquires a lock on the key, executes the critical section, and releases the lock.
|
|
6
|
+
*/
|
|
7
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
8
|
+
}
|
|
9
|
+
export declare class LocalFileStorage<T> implements StorageAdapter<T> {
|
|
10
|
+
private baseDir;
|
|
11
|
+
constructor(options?: {
|
|
12
|
+
baseDir?: string;
|
|
13
|
+
});
|
|
14
|
+
private getFilePath;
|
|
15
|
+
get(key: string): Promise<T | null>;
|
|
16
|
+
set(key: string, value: T): Promise<void>;
|
|
17
|
+
withLock<R>(key: string, fn: () => Promise<R>): Promise<R>;
|
|
18
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LocalFileStorage = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const lockfile = __importStar(require("proper-lockfile"));
|
|
40
|
+
class LocalFileStorage {
|
|
41
|
+
baseDir;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.baseDir = options.baseDir || path.join(process.cwd(), '.agent-diaries');
|
|
44
|
+
if (!fs.existsSync(this.baseDir)) {
|
|
45
|
+
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
getFilePath(key) {
|
|
49
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
50
|
+
return path.join(this.baseDir, `${safeKey}.json`);
|
|
51
|
+
}
|
|
52
|
+
async get(key) {
|
|
53
|
+
const filePath = this.getFilePath(key);
|
|
54
|
+
if (!fs.existsSync(filePath)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const data = await fs.promises.readFile(filePath, 'utf-8');
|
|
59
|
+
return JSON.parse(data);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error(`[LocalFileStorage] Error reading key ${key}:`, e);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async set(key, value) {
|
|
67
|
+
const filePath = this.getFilePath(key);
|
|
68
|
+
try {
|
|
69
|
+
const data = JSON.stringify(value, null, 2);
|
|
70
|
+
await fs.promises.writeFile(filePath, data, 'utf-8');
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
console.error(`[LocalFileStorage] Error writing key ${key}:`, e);
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async withLock(key, fn) {
|
|
78
|
+
const filePath = this.getFilePath(key);
|
|
79
|
+
// Ensure the file exists so we can lock it (proper-lockfile requires the file/dir to exist)
|
|
80
|
+
if (!fs.existsSync(filePath)) {
|
|
81
|
+
await fs.promises.writeFile(filePath, "null", 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
const release = await lockfile.lock(filePath, {
|
|
84
|
+
retries: { retries: 100, minTimeout: 10, maxTimeout: 100 },
|
|
85
|
+
realpath: false
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
return await fn();
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await release();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.LocalFileStorage = LocalFileStorage;
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-diaries/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The lightweight, framework-agnostic memory layer for edge AI agents.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:coverage": "vitest run --coverage",
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"agent",
|
|
19
|
+
"memory",
|
|
20
|
+
"diary",
|
|
21
|
+
"local"
|
|
22
|
+
],
|
|
23
|
+
"author": "swapwarick_n",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/swapwarick/agent-diaries-sdk.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/swapwarick/agent-diaries-sdk/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/swapwarick/agent-diaries-sdk#readme",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"@types/pg": "^8.20.0",
|
|
36
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
37
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
38
|
+
"ts-node": "^10.9.2",
|
|
39
|
+
"typescript": "^5.0.0",
|
|
40
|
+
"vitest": "^4.1.5"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"ioredis": "^5.10.1",
|
|
44
|
+
"pg": "^8.20.0",
|
|
45
|
+
"proper-lockfile": "^4.1.2"
|
|
46
|
+
}
|
|
47
|
+
}
|