saico 2.8.1 → 2.9.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 +43 -28
- package/index.js +10 -9
- package/itask.js +3 -0
- package/msgs.js +9 -1
- package/package.json +1 -1
- package/redis.js +7 -5
- package/saico.js +167 -136
- package/store.js +1 -126
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Saico - Hierarchical AI Conversation Orchestrator
|
|
2
2
|
|
|
3
|
-
Saico is a Node.js library for building AI agents with hierarchical conversations, automatic context aggregation, and enterprise-grade tool calling. It manages nested task trees where each node can have its own
|
|
3
|
+
Saico is a Node.js library for building AI agents with hierarchical conversations, automatic context aggregation, and enterprise-grade tool calling. It manages nested task trees where each node can have its own message queue, system prompt, tools, and state — and the library automatically assembles the full payload sent to the LLM by walking the tree.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Hierarchical conversations** — Parent-child task trees with automatic prompt, tool, and state summary aggregation
|
|
8
8
|
- **Token-aware summarization** — Automatic summarization when message history approaches token limits
|
|
9
9
|
- **Tool calling** — Depth control, deferred execution, duplicate detection, repetition prevention, and timeout handling
|
|
10
|
-
- **Pluggable storage** — Optional Redis persistence (auto-save via proxy) and pluggable DB backends (DynamoDB adapter included)
|
|
10
|
+
- **Pluggable storage** — Optional Redis persistence (auto-save via proxy), library-level backend registration (`Saico.registerBackend`), and pluggable DB backends (DynamoDB adapter included)
|
|
11
11
|
- **Isolation boundaries** — `opt.isolate` stops ancestor aggregation at any node in the tree
|
|
12
12
|
- **Serialization** — Full state save/restore for long-running agents
|
|
13
13
|
|
|
@@ -54,7 +54,7 @@ agent.activate({ createQ: true });
|
|
|
54
54
|
// Backend message (prefixed with [BACKEND] automatically)
|
|
55
55
|
const reply = await agent.sendMessage('What is the weather in Tokyo?');
|
|
56
56
|
|
|
57
|
-
// User-facing chat message (routed to deepest active
|
|
57
|
+
// User-facing chat message (routed to deepest active msgs Q)
|
|
58
58
|
const chatReply = await agent.recvChatMessage('Hello!');
|
|
59
59
|
```
|
|
60
60
|
|
|
@@ -83,7 +83,7 @@ agent.activate();
|
|
|
83
83
|
await agent.sendMessage('Do something');
|
|
84
84
|
await agent.recvChatMessage('User says hello');
|
|
85
85
|
|
|
86
|
-
// 4. Deactivate — bubbles cleaned messages to parent, closes
|
|
86
|
+
// 4. Deactivate — bubbles cleaned messages to parent, closes msgs Q
|
|
87
87
|
await agent.deactivate();
|
|
88
88
|
```
|
|
89
89
|
|
|
@@ -124,7 +124,7 @@ Root Saico (prompt: "You are a manager")
|
|
|
124
124
|
Functions aggregated from all levels.
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
- **`sendMessage(content, functions, opts)`** — Sends a backend message (auto-prefixed `[BACKEND]`). Uses the current or nearest ancestor
|
|
127
|
+
- **`sendMessage(content, functions, opts)`** — Sends a backend message (auto-prefixed `[BACKEND]`). Uses the current or nearest ancestor msgs Q.
|
|
128
128
|
- **`recvChatMessage(content, opts)`** — Routes a user chat message DOWN to the deepest descendant with a message queue.
|
|
129
129
|
|
|
130
130
|
### Isolation
|
|
@@ -151,12 +151,12 @@ class OrderAgent extends Saico {
|
|
|
151
151
|
}
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
When a Saico's
|
|
154
|
+
When a Saico's msgs Q is not the deepest active one, its last 5 user/assistant messages are also included in the state summary automatically.
|
|
155
155
|
|
|
156
156
|
### Spawning Child Saico Instances
|
|
157
157
|
|
|
158
158
|
```js
|
|
159
|
-
// Child with its own
|
|
159
|
+
// Child with its own msgs Q (auto-activated by spawn)
|
|
160
160
|
const child = new Saico({
|
|
161
161
|
name: 'subtask',
|
|
162
162
|
prompt: 'Handle this specific sub-task',
|
|
@@ -166,7 +166,7 @@ const child = new Saico({
|
|
|
166
166
|
agent.spawn(child);
|
|
167
167
|
await child.sendMessage('Working on subtask...');
|
|
168
168
|
|
|
169
|
-
// Child without
|
|
169
|
+
// Child without msgs Q (uses parent's via findMsgs())
|
|
170
170
|
const simple = new Saico({ name: 'simple' });
|
|
171
171
|
agent.spawn(simple);
|
|
172
172
|
await simple.sendMessage('Quick operation');
|
|
@@ -210,7 +210,8 @@ new Saico({
|
|
|
210
210
|
// Storage
|
|
211
211
|
redis: true, // Set false to skip Redis proxy
|
|
212
212
|
key: 'custom-redis-key',
|
|
213
|
-
|
|
213
|
+
store: 'my-table', // Table name for instance persistence (closeSession/rehydrate)
|
|
214
|
+
dynamodb: { // DynamoDB config (creates instance-level adapter)
|
|
214
215
|
region: 'us-east-1',
|
|
215
216
|
credentials: { accessKeyId: '...', secretAccessKey: '...' },
|
|
216
217
|
},
|
|
@@ -257,15 +258,15 @@ agent.getSessionInfo();
|
|
|
257
258
|
// userData, uptime
|
|
258
259
|
// }
|
|
259
260
|
|
|
260
|
-
await agent.closeSession(); //
|
|
261
|
+
await agent.closeSession(); // prepareForStorage + save to registered backend, cancels task
|
|
261
262
|
|
|
262
|
-
// Restore from
|
|
263
|
-
const restored = await Saico.rehydrate(agent.id, { store });
|
|
263
|
+
// Restore from registered backend
|
|
264
|
+
const restored = await Saico.rehydrate(agent.id, { store: 'sessions' });
|
|
264
265
|
```
|
|
265
266
|
|
|
266
267
|
## Database Access
|
|
267
268
|
|
|
268
|
-
Saico provides backend-agnostic DB methods. Configure via `opt.dynamodb` (auto-creates
|
|
269
|
+
Saico provides backend-agnostic DB methods. Configure via `Saico.registerBackend('dynamodb', config)` (library-level), `opt.dynamodb` (instance-level auto-creates adapter), or `opt.db` (any adapter). Table name is required on every call. Child Saico instances without their own DB inherit the parent's adapter automatically via `_getDb()`, which also falls back to the registered backend.
|
|
269
270
|
|
|
270
271
|
```js
|
|
271
272
|
// CRUD — table name required on every call
|
|
@@ -300,28 +301,42 @@ class MyAgent extends Saico {
|
|
|
300
301
|
|
|
301
302
|
## Serialization
|
|
302
303
|
|
|
304
|
+
Both `serialize()` and `Saico.deserialize()` are async. `serialize()` calls `prepareForStorage()` first (strips `_` props, skips functions/states, compresses msgs) then `JSON.stringify`s the result.
|
|
305
|
+
|
|
303
306
|
```js
|
|
304
|
-
//
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
+
// prepareForStorage — clean snapshot
|
|
308
|
+
const data = await agent.prepareForStorage();
|
|
309
|
+
|
|
310
|
+
// serialize/deserialize
|
|
311
|
+
const json = await agent.serialize();
|
|
312
|
+
const restored = await Saico.deserialize(json);
|
|
307
313
|
|
|
308
|
-
// Durable persistence (
|
|
314
|
+
// Durable persistence (uses registered backend + opt.store table name)
|
|
309
315
|
await agent.closeSession();
|
|
310
|
-
const restored2 = await Saico.rehydrate(agent.id, { store });
|
|
316
|
+
const restored2 = await Saico.rehydrate(agent.id, { store: 'sessions' });
|
|
311
317
|
```
|
|
312
318
|
|
|
313
|
-
`
|
|
314
|
-
|
|
315
|
-
## Redis Persistence
|
|
319
|
+
`prepareForStorage()` automatically picks up all non-underscore properties (id, name, prompt, userData, sessionConfig, tm_create, isolate, etc.) and produces compressed chat_history for the msgs Q.
|
|
316
320
|
|
|
317
|
-
|
|
321
|
+
## Initialization
|
|
318
322
|
|
|
319
323
|
```js
|
|
320
|
-
const { init } = require('saico');
|
|
324
|
+
const { Saico, init } = require('saico');
|
|
325
|
+
|
|
326
|
+
// Initialize Redis (default: enabled) and register DynamoDB backend
|
|
327
|
+
await init({
|
|
328
|
+
dynamodb: { region: 'us-east-1', credentials: { accessKeyId: 'AK', secretAccessKey: 'SK' } },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Or register backend directly
|
|
332
|
+
Saico.registerBackend('dynamodb', { region: 'us-east-1', credentials: { ... } });
|
|
333
|
+
```
|
|
321
334
|
|
|
322
|
-
|
|
323
|
-
|
|
335
|
+
## Redis Persistence
|
|
336
|
+
|
|
337
|
+
When Redis is initialized (default: enabled via `init()`), Saico instances are automatically wrapped in an observable proxy. Any property change triggers a debounced save to Redis.
|
|
324
338
|
|
|
339
|
+
```js
|
|
325
340
|
const agent = new Saico({ name: 'persistent-agent' });
|
|
326
341
|
agent.someProperty = 'value'; // Auto-saved to Redis
|
|
327
342
|
```
|
|
@@ -374,11 +389,11 @@ const reply = await ctx.sendMessage('user', 'Hello', functions);
|
|
|
374
389
|
```
|
|
375
390
|
saico/
|
|
376
391
|
+-- index.js # Thin barrel file, exports all components
|
|
377
|
-
+-- saico.js # Saico master class — owns
|
|
392
|
+
+-- saico.js # Saico master class — owns msgs Q, spawn, DB, orchestration
|
|
378
393
|
+-- itask.js # Pure task runner — hierarchy, states, cancellation, promises
|
|
379
394
|
+-- msgs.js # Conversation context (message queue, tool calls, summarization)
|
|
380
395
|
+-- dynamo.js # DynamoDB storage adapter
|
|
381
|
-
+-- store.js #
|
|
396
|
+
+-- store.js # Minimal storage shell (Redis helper + ID generation)
|
|
382
397
|
+-- openai.js # OpenAI API wrapper with retry logic
|
|
383
398
|
+-- redis.js # Redis persistence with observable proxy
|
|
384
399
|
+-- util.js # Utilities (token counting, logging)
|
|
@@ -390,7 +405,7 @@ saico/
|
|
|
390
405
|
npm test
|
|
391
406
|
```
|
|
392
407
|
|
|
393
|
-
|
|
408
|
+
300 tests covering Saico lifecycle, msgs Q ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, async serialization, prepareForStorage, backend registration, persistence (closeSession/rehydrate via registered backend), storage integration, and full hierarchy flows.
|
|
394
409
|
|
|
395
410
|
## Requirements
|
|
396
411
|
|
package/index.js
CHANGED
|
@@ -2,28 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
const Itask = require('./itask.js');
|
|
4
4
|
const { Msgs, createMsgs } = require('./msgs.js');
|
|
5
|
-
const { Store
|
|
5
|
+
const { Store } = require('./store.js');
|
|
6
6
|
const { Saico } = require('./saico.js');
|
|
7
7
|
const { DynamoDBAdapter } = require('./dynamo.js');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Initialize Saico with storage configuration.
|
|
11
|
-
*
|
|
11
|
+
* Registers the backend and optionally initializes Redis.
|
|
12
12
|
*
|
|
13
13
|
* @param {Object} config - Configuration options
|
|
14
|
-
* @param {boolean} config.redis -
|
|
15
|
-
* @param {Object} config.dynamodb - DynamoDB
|
|
14
|
+
* @param {boolean} [config.redis=true] - Set false to skip Redis init
|
|
15
|
+
* @param {Object} [config.dynamodb] - DynamoDB config { region, credentials, client }
|
|
16
16
|
* @returns {Store} The initialized Store instance
|
|
17
17
|
*/
|
|
18
18
|
async function init(config = {}) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (config.redis) {
|
|
19
|
+
if (config.redis !== false) {
|
|
22
20
|
const redis = require('./redis.js');
|
|
23
21
|
await redis.init();
|
|
24
|
-
store.setRedis(redis.rclient);
|
|
25
22
|
}
|
|
26
23
|
|
|
24
|
+
if (config.dynamodb)
|
|
25
|
+
Saico.registerBackend('dynamodb', config.dynamodb);
|
|
26
|
+
|
|
27
|
+
// Legacy: still init Store shell
|
|
28
|
+
const store = Store.init(config);
|
|
27
29
|
return store;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -36,7 +38,6 @@ module.exports = {
|
|
|
36
38
|
Itask,
|
|
37
39
|
Msgs,
|
|
38
40
|
Store,
|
|
39
|
-
DynamoBackend,
|
|
40
41
|
|
|
41
42
|
// Initialization
|
|
42
43
|
init,
|
package/itask.js
CHANGED
|
@@ -534,6 +534,9 @@ Itask.prototype.continue = function(ret){
|
|
|
534
534
|
return this;
|
|
535
535
|
};
|
|
536
536
|
|
|
537
|
+
/* ---------- serialization ---------- */
|
|
538
|
+
Itask.prototype.serialize = function(){ return JSON.stringify({}); };
|
|
539
|
+
|
|
537
540
|
/* ---------- introspection / ps ---------- */
|
|
538
541
|
Itask.prototype.is_running = function(){ return this.running && !this._completed; };
|
|
539
542
|
Itask.prototype.is_completed = function(){ return this._completed; };
|
package/msgs.js
CHANGED
|
@@ -393,7 +393,15 @@ class Msgs {
|
|
|
393
393
|
return this._msgs.length;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
serialize() {
|
|
396
|
+
async serialize() {
|
|
397
|
+
const { chat_history, tool_digest } = await this.prepareForStorage();
|
|
398
|
+
return JSON.stringify({
|
|
399
|
+
tag: this.tag,
|
|
400
|
+
chat_history,
|
|
401
|
+
tool_digest,
|
|
402
|
+
functions: this.functions,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
397
405
|
|
|
398
406
|
getSummaries() { return this._msgs.filter(m => m.opts.summary); }
|
|
399
407
|
|
package/package.json
CHANGED
package/redis.js
CHANGED
|
@@ -39,11 +39,13 @@ function createObservableForRedis(key, obj) {
|
|
|
39
39
|
let lastSavedObject = null; // Cache for the last-saved sanitized object
|
|
40
40
|
let lastSavedTimestamp = null; // Timestamp of the last save to Redis
|
|
41
41
|
|
|
42
|
-
const saveToRedis = debounce(() => {
|
|
42
|
+
const saveToRedis = debounce(async () => {
|
|
43
43
|
const sanitizedObj = sanitizeObject(obj);
|
|
44
44
|
|
|
45
45
|
// Compare sanitized object with the last-saved object
|
|
46
|
-
|
|
46
|
+
const serializedNew = await serialize(sanitizedObj);
|
|
47
|
+
const serializedOld = await serialize(lastSavedObject);
|
|
48
|
+
if (serializedNew === serializedOld) {
|
|
47
49
|
logDebug("No changes detected, skipping save.");
|
|
48
50
|
return;
|
|
49
51
|
}
|
|
@@ -51,7 +53,7 @@ function createObservableForRedis(key, obj) {
|
|
|
51
53
|
lastSavedObject = sanitizedObj;
|
|
52
54
|
lastSavedTimestamp = Date.now(); // Update the last saved timestamp
|
|
53
55
|
sanitizedObj.lastSave = lastSavedTimestamp;
|
|
54
|
-
rclient.set(key,
|
|
56
|
+
await rclient.set(key, serializedNew);
|
|
55
57
|
logDebug("Saved to Redis:", key, `at ${lastSavedTimestamp}`);
|
|
56
58
|
}, 1000);
|
|
57
59
|
|
|
@@ -100,9 +102,9 @@ function createObservableForRedis(key, obj) {
|
|
|
100
102
|
},
|
|
101
103
|
};
|
|
102
104
|
|
|
103
|
-
function serialize(obj) {
|
|
105
|
+
async function serialize(obj) {
|
|
104
106
|
if (typeof obj == 'object' && typeof obj?.serialize == 'function')
|
|
105
|
-
return obj.serialize();
|
|
107
|
+
return await obj.serialize();
|
|
106
108
|
return JSON.stringify(obj);
|
|
107
109
|
}
|
|
108
110
|
|
package/saico.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const Itask = require('./itask.js');
|
|
5
5
|
const { Msgs } = require('./msgs.js');
|
|
6
|
-
const { Store } = require('./store.js');
|
|
7
6
|
const util = require('./util.js');
|
|
8
7
|
|
|
9
8
|
function makeId(len = 12){
|
|
@@ -19,7 +18,7 @@ function makeId(len = 12){
|
|
|
19
18
|
* - Construction: sets up storage (Redis observable + optional DynamoDB),
|
|
20
19
|
* class-level prompt, tool config. No Itask is created yet.
|
|
21
20
|
* - activate(opts): creates the internal Itask and optionally attaches a
|
|
22
|
-
* message Q
|
|
21
|
+
* message Q (when opts.createQ is true).
|
|
23
22
|
* - DB access works before and after activation.
|
|
24
23
|
*
|
|
25
24
|
* Saico orchestrates the full message payload sent to the LLM by walking its
|
|
@@ -37,11 +36,11 @@ class Saico {
|
|
|
37
36
|
* @param {Array} [opt.functions] - Available AI functions
|
|
38
37
|
* @param {string} [opt.key] - Redis key override (default: 'saico:<id>')
|
|
39
38
|
* @param {boolean} [opt.redis=true] - Set false to skip Redis proxy
|
|
40
|
-
* @param {boolean} [opt.createQ] - Create message Q
|
|
39
|
+
* @param {boolean} [opt.createQ] - Create message Q on activate()
|
|
41
40
|
* @param {boolean} [opt.isolate] - Isolate: don't aggregate from ancestors
|
|
42
41
|
* @param {Object} [opt.dynamodb] - DynamoDB config { region, credentials: { accessKeyId, secretAccessKey }, client }
|
|
43
42
|
* @param {Object} [opt.db] - Pluggable DB backend
|
|
44
|
-
* @param {
|
|
43
|
+
* @param {string} [opt.store] - Table name for instance persistence
|
|
45
44
|
* @param {Object} [opt.userData] - Initial user data
|
|
46
45
|
* @param {Object} [opt.sessionConfig] - Session config overrides
|
|
47
46
|
*/
|
|
@@ -49,13 +48,13 @@ class Saico {
|
|
|
49
48
|
// Internal properties (underscore-prefixed, not persisted to Redis)
|
|
50
49
|
this.id = opt.id || crypto.randomBytes(8).toString('hex');
|
|
51
50
|
this._task = null;
|
|
52
|
-
this.
|
|
51
|
+
this._storeName = (typeof opt.store === 'string') ? opt.store : null;
|
|
53
52
|
this._opt = opt;
|
|
54
|
-
this.
|
|
53
|
+
this.isolate = opt.isolate || false;
|
|
55
54
|
|
|
56
|
-
//
|
|
57
|
-
this.
|
|
58
|
-
this.
|
|
55
|
+
// Msgs Q owned directly by Saico (not Itask)
|
|
56
|
+
this.msgs = null;
|
|
57
|
+
this.msgs_id = null;
|
|
59
58
|
|
|
60
59
|
// Public configuration
|
|
61
60
|
this.name = opt.name || this.constructor.name || 'saico';
|
|
@@ -98,7 +97,7 @@ class Saico {
|
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
/**
|
|
101
|
-
* Create the internal Itask and optionally a message Q
|
|
100
|
+
* Create the internal Itask and optionally a message Q.
|
|
102
101
|
*
|
|
103
102
|
* @param {Object} opts
|
|
104
103
|
* @param {boolean} [opts.createQ] - Override this.createQ for this activation
|
|
@@ -106,7 +105,7 @@ class Saico {
|
|
|
106
105
|
* @param {Array} [opts.functions] - Override functions
|
|
107
106
|
* @param {Array} [opts.states] - Override this.states for this activation
|
|
108
107
|
* @param {string} [opts.taskId] - Custom task ID
|
|
109
|
-
* @param {number} [opts.token_limit] - Token limit for
|
|
108
|
+
* @param {number} [opts.token_limit] - Token limit for msgs Q
|
|
110
109
|
* @param {number} [opts.max_depth] - Max tool call depth
|
|
111
110
|
* @param {number} [opts.max_tool_repetition] - Max tool repetition
|
|
112
111
|
* @param {number} [opts.queue_limit] - Message queue limit
|
|
@@ -114,14 +113,22 @@ class Saico {
|
|
|
114
113
|
* @param {boolean} [opts.sequential_mode] - Sequential message processing
|
|
115
114
|
* @param {Array} [opts.msgs] - Initial messages
|
|
116
115
|
* @param {*} [opts.chat_history] - Chat history to restore
|
|
117
|
-
* @param {Object} [opts.
|
|
116
|
+
* @param {Object} [opts.msgsConfig] - Additional Msgs config overrides
|
|
118
117
|
* @returns {Saico} this instance (for chaining)
|
|
119
118
|
*/
|
|
120
119
|
activate(opts = {}) {
|
|
121
120
|
if (this._task)
|
|
122
121
|
throw new Error('Already activated. Call deactivate() first.');
|
|
123
122
|
|
|
124
|
-
const
|
|
123
|
+
const defaultStates = [
|
|
124
|
+
async function main() {
|
|
125
|
+
return this._task.wait();
|
|
126
|
+
},
|
|
127
|
+
async function catch$error_handler(err) {
|
|
128
|
+
console.error(`${this.name} caught error:`, err);
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
const states = opts.states || this.states || defaultStates;
|
|
125
132
|
|
|
126
133
|
// Build effective prompt: class-level + activation-level
|
|
127
134
|
const effectivePrompt = [this.prompt, opts.prompt].filter(Boolean).join('\n');
|
|
@@ -138,10 +145,10 @@ class Saico {
|
|
|
138
145
|
// Store Saico reference on task for parent chain traversal
|
|
139
146
|
this._task._saico = this;
|
|
140
147
|
|
|
141
|
-
// Create message Q
|
|
148
|
+
// Create message Q if requested (class-level or activate-level)
|
|
142
149
|
if (opts.createQ ?? this.createQ) {
|
|
143
150
|
const functions = opts.functions || this.functions;
|
|
144
|
-
const
|
|
151
|
+
const msgsConfig = {
|
|
145
152
|
tag: opts.tag || this._task.id,
|
|
146
153
|
token_limit: opts.token_limit ?? this.sessionConfig.token_limit,
|
|
147
154
|
max_depth: opts.max_depth ?? this.sessionConfig.max_depth,
|
|
@@ -153,16 +160,16 @@ class Saico {
|
|
|
153
160
|
msgs: opts.msgs,
|
|
154
161
|
chat_history: opts.chat_history,
|
|
155
162
|
tool_digest: opts.tool_digest,
|
|
156
|
-
...opts.
|
|
163
|
+
...opts.msgsConfig,
|
|
157
164
|
};
|
|
158
165
|
|
|
159
166
|
const augmentedPrompt = effectivePrompt
|
|
160
167
|
? effectivePrompt + Saico.BACKEND_EXPLANATION
|
|
161
168
|
: '';
|
|
162
|
-
const msgs = new Msgs(augmentedPrompt,
|
|
163
|
-
this.
|
|
164
|
-
this.
|
|
165
|
-
msgs.tag = this.
|
|
169
|
+
const msgs = new Msgs(augmentedPrompt, msgsConfig);
|
|
170
|
+
this.msgs = msgs;
|
|
171
|
+
this.msgs_id = makeId(16);
|
|
172
|
+
msgs.tag = this.msgs_id;
|
|
166
173
|
|
|
167
174
|
// Wire callbacks for hierarchy access
|
|
168
175
|
msgs._findToolImpl = (toolName) => this._findToolImpl(toolName);
|
|
@@ -175,63 +182,63 @@ class Saico {
|
|
|
175
182
|
// ---- Context management (owned by Saico, not Itask) ----
|
|
176
183
|
|
|
177
184
|
/**
|
|
178
|
-
* Find the nearest
|
|
185
|
+
* Find the nearest msgs Q walking UP the Saico/task hierarchy.
|
|
179
186
|
*/
|
|
180
|
-
|
|
181
|
-
if (this.
|
|
187
|
+
findMsgs() {
|
|
188
|
+
if (this.msgs) return this.msgs;
|
|
182
189
|
let task = this._task?.parent;
|
|
183
190
|
while (task) {
|
|
184
|
-
if (task._saico?.
|
|
191
|
+
if (task._saico?.msgs) return task._saico.msgs;
|
|
185
192
|
task = task.parent;
|
|
186
193
|
}
|
|
187
194
|
return null;
|
|
188
195
|
}
|
|
189
196
|
|
|
190
197
|
/**
|
|
191
|
-
* Walk DOWN to find the deepest active descendant with a
|
|
198
|
+
* Walk DOWN to find the deepest active descendant with a msgs Q.
|
|
192
199
|
*/
|
|
193
|
-
|
|
194
|
-
if (!this._task) return this.
|
|
195
|
-
let deepest = this.
|
|
200
|
+
findDeepestMsgs() {
|
|
201
|
+
if (!this._task) return this.msgs || null;
|
|
202
|
+
let deepest = this.msgs ? { msgs: this.msgs, depth: 0 } : null;
|
|
196
203
|
const search = (task, depth) => {
|
|
197
204
|
for (const child of task.child) {
|
|
198
205
|
if (child._completed) continue;
|
|
199
|
-
if (child._saico?.
|
|
206
|
+
if (child._saico?.msgs) {
|
|
200
207
|
if (!deepest || depth + 1 >= deepest.depth)
|
|
201
|
-
deepest = {
|
|
208
|
+
deepest = { msgs: child._saico.msgs, depth: depth + 1 };
|
|
202
209
|
}
|
|
203
210
|
search(child, depth + 1);
|
|
204
211
|
}
|
|
205
212
|
};
|
|
206
213
|
search(this._task, 0);
|
|
207
|
-
return deepest ? deepest.
|
|
214
|
+
return deepest ? deepest.msgs : null;
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
/**
|
|
211
|
-
* Deactivate — bubble cleaned messages to parent, close
|
|
218
|
+
* Deactivate — bubble cleaned messages to parent, close msgs Q, cancel task.
|
|
212
219
|
* Pushes cleaned messages (no tool calls, no BACKEND) into the parent's Q,
|
|
213
|
-
* then closes the
|
|
220
|
+
* then closes the msgs Q without the default summary bubbling.
|
|
214
221
|
*/
|
|
215
222
|
async deactivate() {
|
|
216
223
|
if (!this._task) return;
|
|
217
|
-
if (this.
|
|
218
|
-
// Find parent
|
|
224
|
+
if (this.msgs) {
|
|
225
|
+
// Find parent msgs to bubble cleaned messages
|
|
219
226
|
let parentTask = this._task.parent;
|
|
220
|
-
let
|
|
227
|
+
let parentMsgs = null;
|
|
221
228
|
while (parentTask) {
|
|
222
|
-
if (parentTask._saico?.
|
|
229
|
+
if (parentTask._saico?.msgs) { parentMsgs = parentTask._saico.msgs; break; }
|
|
223
230
|
parentTask = parentTask.parent;
|
|
224
231
|
}
|
|
225
|
-
if (
|
|
232
|
+
if (parentMsgs) {
|
|
226
233
|
const cleaned = this.getRecentMessages(Infinity);
|
|
227
234
|
for (const msg of cleaned)
|
|
228
|
-
|
|
235
|
+
parentMsgs.push(msg);
|
|
229
236
|
}
|
|
230
|
-
// Clean tool calls and close
|
|
231
|
-
if (this.
|
|
232
|
-
this.
|
|
233
|
-
this.
|
|
234
|
-
this.
|
|
237
|
+
// Clean tool calls and close msgs Q without additional summary bubbling.
|
|
238
|
+
if (this.msgs_id && typeof this.msgs.cleanToolCallsByTag === 'function')
|
|
239
|
+
this.msgs.cleanToolCallsByTag(this.msgs_id);
|
|
240
|
+
this.msgs = null;
|
|
241
|
+
this.msgs_id = null;
|
|
235
242
|
}
|
|
236
243
|
this._task._ecancel();
|
|
237
244
|
this._task = null;
|
|
@@ -276,7 +283,7 @@ class Saico {
|
|
|
276
283
|
*/
|
|
277
284
|
_getSaicoAncestors() {
|
|
278
285
|
const chain = [this];
|
|
279
|
-
if (this.
|
|
286
|
+
if (this.isolate) return chain;
|
|
280
287
|
let task = this._task?.parent;
|
|
281
288
|
while (task) {
|
|
282
289
|
if (task._saico) {
|
|
@@ -290,7 +297,7 @@ class Saico {
|
|
|
290
297
|
|
|
291
298
|
/**
|
|
292
299
|
* Build preamble and aggregated functions by walking the Saico chain.
|
|
293
|
-
* @param {
|
|
300
|
+
* @param {Msgs} activeCtx - The deepest active msgs Q (for state summary logic)
|
|
294
301
|
* @returns {{ preamble: Array, allFunctions: Array }}
|
|
295
302
|
*/
|
|
296
303
|
_buildPreamble(activeCtx) {
|
|
@@ -317,8 +324,8 @@ class Saico {
|
|
|
317
324
|
}
|
|
318
325
|
|
|
319
326
|
// Tools digest
|
|
320
|
-
if (saico.
|
|
321
|
-
const digestText = saico.
|
|
327
|
+
if (saico.msgs?.tool_digest?.length > 0) {
|
|
328
|
+
const digestText = saico.msgs.tool_digest.map(entry =>
|
|
322
329
|
`[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
|
|
323
330
|
).join('\n');
|
|
324
331
|
preamble.push({ role: 'system', content: '[Tool Activity Log]\n' + digestText });
|
|
@@ -337,20 +344,20 @@ class Saico {
|
|
|
337
344
|
if (!this._task)
|
|
338
345
|
throw new Error('Not activated. Call activate() first.');
|
|
339
346
|
|
|
340
|
-
// Find the active
|
|
341
|
-
let ctx = this.
|
|
347
|
+
// Find the active msgs Q (own or walk up)
|
|
348
|
+
let ctx = this.findMsgs();
|
|
342
349
|
if (!ctx)
|
|
343
|
-
throw new Error('No
|
|
350
|
+
throw new Error('No msgs Q available');
|
|
344
351
|
|
|
345
352
|
// Build preamble by walking Saico chain
|
|
346
|
-
const activeCtx = this.
|
|
353
|
+
const activeCtx = this.findDeepestMsgs() || ctx;
|
|
347
354
|
const { preamble, allFunctions } = this._buildPreamble(activeCtx);
|
|
348
355
|
|
|
349
356
|
// Merge with call-specific functions
|
|
350
357
|
if (functions) allFunctions.push(...(Array.isArray(functions) ? functions : [functions]));
|
|
351
358
|
|
|
352
359
|
opts = Object.assign({}, opts, {
|
|
353
|
-
tag: this.
|
|
360
|
+
tag: this.msgs_id,
|
|
354
361
|
_preamble: preamble,
|
|
355
362
|
_aggregatedFunctions: allFunctions.length > 0 ? allFunctions : null,
|
|
356
363
|
});
|
|
@@ -362,9 +369,9 @@ class Saico {
|
|
|
362
369
|
throw new Error('Not activated. Call activate() first.');
|
|
363
370
|
|
|
364
371
|
// Route DOWN to deepest descendant with a msg Q
|
|
365
|
-
const ctx = this.
|
|
372
|
+
const ctx = this.findDeepestMsgs();
|
|
366
373
|
if (!ctx)
|
|
367
|
-
throw new Error('No
|
|
374
|
+
throw new Error('No msgs Q available');
|
|
368
375
|
|
|
369
376
|
// Build preamble by walking Saico chain
|
|
370
377
|
const { preamble, allFunctions } = this._buildPreamble(ctx);
|
|
@@ -396,8 +403,8 @@ class Saico {
|
|
|
396
403
|
* @returns {Array<{role: string, content: string}>}
|
|
397
404
|
*/
|
|
398
405
|
getRecentMessages(n = 5) {
|
|
399
|
-
if (!this.
|
|
400
|
-
return this.
|
|
406
|
+
if (!this.msgs) return [];
|
|
407
|
+
return this.msgs._msgs
|
|
401
408
|
.filter(m => {
|
|
402
409
|
if (m.msg.role === 'tool' || m.msg.tool_calls) return false;
|
|
403
410
|
if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]')) return false;
|
|
@@ -409,8 +416,8 @@ class Saico {
|
|
|
409
416
|
|
|
410
417
|
/**
|
|
411
418
|
* Internal state summary builder. Includes own getStateSummary() and,
|
|
412
|
-
* if this
|
|
413
|
-
* @param {
|
|
419
|
+
* if this msgs Q is NOT the active (deepest) Q, includes recent messages.
|
|
420
|
+
* @param {Msgs} activeCtx - The deepest active msgs Q
|
|
414
421
|
* @returns {Array|string|null}
|
|
415
422
|
*/
|
|
416
423
|
_getStateSummary(activeCtx) {
|
|
@@ -418,8 +425,8 @@ class Saico {
|
|
|
418
425
|
const own = this.getStateSummary();
|
|
419
426
|
if (own) parts.push(own);
|
|
420
427
|
|
|
421
|
-
// If this
|
|
422
|
-
if (this.
|
|
428
|
+
// If this msgs Q is NOT the active (deepest) Q, include recent messages
|
|
429
|
+
if (this.msgs && activeCtx && this.msgs !== activeCtx) {
|
|
423
430
|
const recent = this.getRecentMessages(5);
|
|
424
431
|
if (recent.length > 0) parts.push(...recent);
|
|
425
432
|
}
|
|
@@ -486,7 +493,7 @@ class Saico {
|
|
|
486
493
|
name: this.name,
|
|
487
494
|
running: this._task?.running || false,
|
|
488
495
|
completed: this._task?._completed || false,
|
|
489
|
-
messageCount: this.
|
|
496
|
+
messageCount: this.msgs?.length || 0,
|
|
490
497
|
childCount: this._task?.child?.size || 0,
|
|
491
498
|
userData: this.userData,
|
|
492
499
|
uptime: Date.now() - this.tm_create,
|
|
@@ -494,35 +501,17 @@ class Saico {
|
|
|
494
501
|
}
|
|
495
502
|
|
|
496
503
|
/**
|
|
497
|
-
* Close the session —
|
|
498
|
-
* The saved object has the same shape as serialize() but with compressed
|
|
499
|
-
* context messages (chat_history) instead of raw _msgs.
|
|
504
|
+
* Close the session — save state to registered backend, cancel task.
|
|
500
505
|
*/
|
|
501
506
|
async closeSession() {
|
|
502
507
|
if (!this._task) return;
|
|
503
508
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
name: this.name,
|
|
511
|
-
prompt: this.prompt,
|
|
512
|
-
userData: this.userData,
|
|
513
|
-
sessionConfig: this.sessionConfig,
|
|
514
|
-
tm_create: this.tm_create,
|
|
515
|
-
isolate: this._isolate,
|
|
516
|
-
taskId: this._task.id,
|
|
517
|
-
context_id: this.context_id,
|
|
518
|
-
context: {
|
|
519
|
-
tag: this.context.tag,
|
|
520
|
-
chat_history,
|
|
521
|
-
tool_digest,
|
|
522
|
-
functions: this.context.functions,
|
|
523
|
-
},
|
|
524
|
-
};
|
|
525
|
-
await store.save(this.id, data);
|
|
509
|
+
if (this._storeName && this.msgs) {
|
|
510
|
+
const backend = Saico.getBackend();
|
|
511
|
+
if (backend) {
|
|
512
|
+
const data = await this.prepareForStorage();
|
|
513
|
+
await backend.put(data, this._storeName);
|
|
514
|
+
}
|
|
526
515
|
}
|
|
527
516
|
|
|
528
517
|
this._task._ecancel();
|
|
@@ -541,7 +530,8 @@ class Saico {
|
|
|
541
530
|
if (task._saico?._db) return task._saico._db;
|
|
542
531
|
task = task.parent;
|
|
543
532
|
}
|
|
544
|
-
|
|
533
|
+
if (Saico._backend) return Saico._backend;
|
|
534
|
+
throw new Error('No DB backend configured. Call Saico.registerBackend() or set opt.db.');
|
|
545
535
|
}
|
|
546
536
|
|
|
547
537
|
async dbPutItem(item, table) {
|
|
@@ -628,42 +618,64 @@ class Saico {
|
|
|
628
618
|
|
|
629
619
|
// ---- Serialization ----
|
|
630
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Prepare this instance for storage. Creates a clean snapshot:
|
|
623
|
+
* - Strips all '_' prefixed properties
|
|
624
|
+
* - Strips functions (including states)
|
|
625
|
+
* - Builds compressed chat_history from msgs Q (via Msgs.prepareForStorage)
|
|
626
|
+
* - Adds taskId from internal Itask
|
|
627
|
+
* @returns {Promise<Object>} Plain serializable object
|
|
628
|
+
*/
|
|
629
|
+
async prepareForStorage() {
|
|
630
|
+
const data = {};
|
|
631
|
+
for (const key of Object.keys(this)) {
|
|
632
|
+
if (key.startsWith('_')) continue;
|
|
633
|
+
if (typeof this[key] === 'function') continue;
|
|
634
|
+
if (key === 'msgs') continue; // handled specially below
|
|
635
|
+
if (key === 'states') continue; // function array, not serializable
|
|
636
|
+
data[key] = this[key];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Deep clone to detach from live instance
|
|
640
|
+
const cloned = JSON.parse(JSON.stringify(data));
|
|
641
|
+
|
|
642
|
+
// Handle msgs — compress via Msgs.prepareForStorage
|
|
643
|
+
if (this.msgs) {
|
|
644
|
+
const { chat_history, tool_digest } = await this.msgs.prepareForStorage();
|
|
645
|
+
cloned.msgs = {
|
|
646
|
+
tag: this.msgs.tag,
|
|
647
|
+
chat_history,
|
|
648
|
+
tool_digest,
|
|
649
|
+
functions: this.msgs.functions,
|
|
650
|
+
};
|
|
651
|
+
} else {
|
|
652
|
+
cloned.msgs = null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Derived properties from underscore-prefixed internals
|
|
656
|
+
cloned.taskId = this._task?.id || null;
|
|
657
|
+
|
|
658
|
+
return cloned;
|
|
659
|
+
}
|
|
660
|
+
|
|
631
661
|
/**
|
|
632
662
|
* Serialize the Saico instance to a JSON string.
|
|
633
|
-
*
|
|
634
|
-
* For durable storage with compressed msgs, use closeSession().
|
|
663
|
+
* Calls prepareForStorage() to build a clean snapshot, then JSON.stringify.
|
|
635
664
|
*/
|
|
636
|
-
serialize() {
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
name: this.name,
|
|
640
|
-
prompt: this.prompt,
|
|
641
|
-
userData: this.userData,
|
|
642
|
-
sessionConfig: this.sessionConfig,
|
|
643
|
-
tm_create: this.tm_create,
|
|
644
|
-
isolate: this._isolate,
|
|
645
|
-
};
|
|
646
|
-
data.taskId = this._task?.id || null;
|
|
647
|
-
data.context_id = this.context_id || null;
|
|
648
|
-
data.context = this.context ? {
|
|
649
|
-
tag: this.context.tag,
|
|
650
|
-
msgs: this.context._msgs,
|
|
651
|
-
functions: this.context.functions,
|
|
652
|
-
tool_digest: this.context.tool_digest,
|
|
653
|
-
} : null;
|
|
654
|
-
return JSON.stringify(data);
|
|
665
|
+
async serialize() {
|
|
666
|
+
const prepared = await this.prepareForStorage();
|
|
667
|
+
return JSON.stringify(prepared);
|
|
655
668
|
}
|
|
656
669
|
|
|
657
670
|
/**
|
|
658
671
|
* Restore a Saico instance from serialized data.
|
|
659
|
-
* Supports both raw msgs (from serialize/Redis) and compressed
|
|
660
|
-
* chat_history (from closeSession/Store).
|
|
661
672
|
* @param {string|Object} data - Serialized data (JSON string or object)
|
|
662
673
|
* @param {Object} opt - Options (functions, store, states, etc.)
|
|
663
|
-
* @returns {Saico}
|
|
674
|
+
* @returns {Promise<Saico>}
|
|
664
675
|
*/
|
|
665
|
-
static deserialize(data, opt = {}) {
|
|
676
|
+
static async deserialize(data, opt = {}) {
|
|
666
677
|
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
678
|
+
const msgsData = parsed.msgs;
|
|
667
679
|
|
|
668
680
|
const instance = new Saico({
|
|
669
681
|
id: parsed.id,
|
|
@@ -672,7 +684,7 @@ class Saico {
|
|
|
672
684
|
userData: parsed.userData,
|
|
673
685
|
sessionConfig: parsed.sessionConfig,
|
|
674
686
|
isolate: parsed.isolate,
|
|
675
|
-
functions: opt.functions ||
|
|
687
|
+
functions: opt.functions || msgsData?.functions,
|
|
676
688
|
store: opt.store,
|
|
677
689
|
redis: false, // No Redis proxy during deserialization
|
|
678
690
|
});
|
|
@@ -681,48 +693,67 @@ class Saico {
|
|
|
681
693
|
|
|
682
694
|
// Activate with restored state if taskId exists
|
|
683
695
|
if (parsed.taskId) {
|
|
684
|
-
const ctx = parsed.context;
|
|
685
696
|
instance.activate({
|
|
686
|
-
createQ: !!
|
|
697
|
+
createQ: !!msgsData,
|
|
687
698
|
taskId: parsed.taskId,
|
|
688
|
-
tag:
|
|
689
|
-
chat_history:
|
|
690
|
-
functions: opt.functions ||
|
|
691
|
-
tool_digest:
|
|
699
|
+
tag: msgsData?.tag,
|
|
700
|
+
chat_history: msgsData?.chat_history,
|
|
701
|
+
functions: opt.functions || msgsData?.functions,
|
|
702
|
+
tool_digest: msgsData?.tool_digest,
|
|
692
703
|
states: opt.states || [],
|
|
693
704
|
...opt,
|
|
694
705
|
});
|
|
695
706
|
|
|
696
|
-
//
|
|
697
|
-
if (
|
|
698
|
-
instance.
|
|
699
|
-
}
|
|
707
|
+
// Decompress chat_history into _msgs
|
|
708
|
+
if (instance.msgs)
|
|
709
|
+
await instance.msgs.initHistory();
|
|
700
710
|
}
|
|
701
711
|
|
|
702
712
|
return instance;
|
|
703
713
|
}
|
|
704
714
|
|
|
705
715
|
/**
|
|
706
|
-
* Load a Saico instance from
|
|
716
|
+
* Load a Saico instance from the registered backend by id.
|
|
707
717
|
* @param {string} id - The Saico instance id
|
|
708
|
-
* @param {Object} opt - Options (store, functions, states, etc.)
|
|
718
|
+
* @param {Object} opt - Options (store: table name, backend, functions, states, etc.)
|
|
709
719
|
* @returns {Promise<Saico|null>}
|
|
710
720
|
*/
|
|
711
721
|
static async rehydrate(id, opt = {}) {
|
|
712
|
-
const
|
|
713
|
-
if (!
|
|
714
|
-
throw new Error('No
|
|
715
|
-
const
|
|
722
|
+
const backend = opt.backend || Saico.getBackend();
|
|
723
|
+
if (!backend)
|
|
724
|
+
throw new Error('No backend registered. Call Saico.registerBackend() first.');
|
|
725
|
+
const table = opt.store;
|
|
726
|
+
if (!table)
|
|
727
|
+
throw new Error('No table specified. Pass opt.store.');
|
|
728
|
+
const data = await backend.get('id', id, table);
|
|
716
729
|
if (!data) return null;
|
|
717
|
-
|
|
718
|
-
// Decompress chat_history into _msgs if present
|
|
719
|
-
if (instance.context)
|
|
720
|
-
await instance.context.initHistory();
|
|
721
|
-
return instance;
|
|
730
|
+
return Saico.deserialize(data, opt);
|
|
722
731
|
}
|
|
723
732
|
}
|
|
724
733
|
|
|
725
|
-
//
|
|
734
|
+
// ---- Static backend registration ----
|
|
735
|
+
|
|
736
|
+
Saico._backend = null;
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Register a storage backend at library level (once, outside instance context).
|
|
740
|
+
* @param {string} type - Backend type ('dynamodb')
|
|
741
|
+
* @param {Object} config - Backend config (passed to adapter constructor)
|
|
742
|
+
*/
|
|
743
|
+
Saico.registerBackend = function(type, config) {
|
|
744
|
+
if (type === 'dynamodb') {
|
|
745
|
+
const { DynamoDBAdapter } = require('./dynamo.js');
|
|
746
|
+
Saico._backend = new DynamoDBAdapter(config);
|
|
747
|
+
} else {
|
|
748
|
+
throw new Error('Unknown backend type: ' + type);
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
Saico.getBackend = function() {
|
|
753
|
+
return Saico._backend;
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// [BACKEND] explanation text appended to msgs Q prompts
|
|
726
757
|
Saico.BACKEND_EXPLANATION = '\nNote: Messages prefixed with [BACKEND] are from the backend ' +
|
|
727
758
|
'server, not the user. They contain server instructions, data updates, or system context. ' +
|
|
728
759
|
'Treat them as authoritative system-level information.';
|
package/store.js
CHANGED
|
@@ -4,46 +4,10 @@ const crypto = require('crypto');
|
|
|
4
4
|
|
|
5
5
|
let _instance = null;
|
|
6
6
|
|
|
7
|
-
class DynamoBackend {
|
|
8
|
-
constructor({ table, aws }) {
|
|
9
|
-
this.table = table;
|
|
10
|
-
this.aws = aws;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async save(id, data) {
|
|
14
|
-
await this.aws.dynamoPutItem(this.table, {
|
|
15
|
-
id,
|
|
16
|
-
data: typeof data === 'string' ? data : JSON.stringify(data),
|
|
17
|
-
updated_at: Date.now()
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async load(id) {
|
|
22
|
-
const item = await this.aws.dynamoGetItem(this.table, 'id', id);
|
|
23
|
-
if (!item)
|
|
24
|
-
return null;
|
|
25
|
-
const data = item.data;
|
|
26
|
-
if (typeof data === 'string') {
|
|
27
|
-
try { return JSON.parse(data); }
|
|
28
|
-
catch (e) { return data; }
|
|
29
|
-
}
|
|
30
|
-
return data;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async delete(id) {
|
|
34
|
-
await this.aws.dynamoDeleteItem(this.table, 'id', id);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
7
|
class Store {
|
|
39
8
|
constructor(config = {}) {
|
|
40
9
|
this._redis = null;
|
|
41
|
-
this._backends = {};
|
|
42
10
|
this._config = config;
|
|
43
|
-
|
|
44
|
-
if (config.dynamodb) {
|
|
45
|
-
this._backends.dynamodb = new DynamoBackend(config.dynamodb);
|
|
46
|
-
}
|
|
47
11
|
}
|
|
48
12
|
|
|
49
13
|
static get instance() {
|
|
@@ -56,7 +20,6 @@ class Store {
|
|
|
56
20
|
|
|
57
21
|
static init(config = {}) {
|
|
58
22
|
_instance = new Store(config);
|
|
59
|
-
// If redis module provided or redis is already initialized, grab the client
|
|
60
23
|
const redis = require('./redis.js');
|
|
61
24
|
if (redis.rclient)
|
|
62
25
|
_instance._redis = redis.rclient;
|
|
@@ -67,97 +30,9 @@ class Store {
|
|
|
67
30
|
this._redis = rclient;
|
|
68
31
|
}
|
|
69
32
|
|
|
70
|
-
addBackend(name, backend) {
|
|
71
|
-
this._backends[name] = backend;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
33
|
generateId() {
|
|
75
34
|
return crypto.randomBytes(8).toString('hex');
|
|
76
35
|
}
|
|
77
|
-
|
|
78
|
-
async save(id, data) {
|
|
79
|
-
const key = 'saico:' + id;
|
|
80
|
-
const serialized = typeof data === 'string' ? data : JSON.stringify(data);
|
|
81
|
-
|
|
82
|
-
// Always save to Redis if available
|
|
83
|
-
if (this._redis) {
|
|
84
|
-
try {
|
|
85
|
-
await this._redis.set(key, serialized);
|
|
86
|
-
} catch (e) {
|
|
87
|
-
console.error('Store: Redis save error:', e.message);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Save to all configured backends
|
|
92
|
-
for (const [name, backend] of Object.entries(this._backends)) {
|
|
93
|
-
try {
|
|
94
|
-
await backend.save(id, data);
|
|
95
|
-
} catch (e) {
|
|
96
|
-
console.error(`Store: ${name} backend save error:`, e.message);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async load(id) {
|
|
102
|
-
const key = 'saico:' + id;
|
|
103
|
-
|
|
104
|
-
// Try Redis first
|
|
105
|
-
if (this._redis) {
|
|
106
|
-
try {
|
|
107
|
-
const cached = await this._redis.get(key);
|
|
108
|
-
if (cached) {
|
|
109
|
-
try { return JSON.parse(cached); }
|
|
110
|
-
catch (e) { return cached; }
|
|
111
|
-
}
|
|
112
|
-
} catch (e) {
|
|
113
|
-
console.error('Store: Redis load error:', e.message);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Fall back to backends
|
|
118
|
-
for (const [name, backend] of Object.entries(this._backends)) {
|
|
119
|
-
try {
|
|
120
|
-
const data = await backend.load(id);
|
|
121
|
-
if (data) {
|
|
122
|
-
// Cache to Redis for next time
|
|
123
|
-
if (this._redis) {
|
|
124
|
-
try {
|
|
125
|
-
const serialized = typeof data === 'string'
|
|
126
|
-
? data : JSON.stringify(data);
|
|
127
|
-
await this._redis.set(key, serialized);
|
|
128
|
-
} catch (e) {
|
|
129
|
-
console.error('Store: Redis cache-back error:', e.message);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return data;
|
|
133
|
-
}
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.error(`Store: ${name} backend load error:`, e.message);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async delete(id) {
|
|
143
|
-
const key = 'saico:' + id;
|
|
144
|
-
|
|
145
|
-
if (this._redis) {
|
|
146
|
-
try {
|
|
147
|
-
await this._redis.del(key);
|
|
148
|
-
} catch (e) {
|
|
149
|
-
console.error('Store: Redis delete error:', e.message);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
for (const [name, backend] of Object.entries(this._backends)) {
|
|
154
|
-
try {
|
|
155
|
-
await backend.delete(id);
|
|
156
|
-
} catch (e) {
|
|
157
|
-
console.error(`Store: ${name} backend delete error:`, e.message);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
36
|
}
|
|
162
37
|
|
|
163
|
-
module.exports = { Store
|
|
38
|
+
module.exports = { Store };
|