saico 2.8.0 → 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 +48 -33
- package/index.js +13 -12
- package/itask.js +3 -0
- package/msgs.js +38 -182
- package/package.json +1 -1
- package/redis.js +7 -5
- package/saico.js +210 -161
- package/store.js +1 -126
package/saico.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const Itask = require('./itask.js');
|
|
5
|
-
const {
|
|
6
|
-
const { Store } = require('./store.js');
|
|
5
|
+
const { Msgs } = require('./msgs.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,25 +36,25 @@ 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
|
*/
|
|
48
47
|
constructor(opt = {}) {
|
|
49
48
|
// Internal properties (underscore-prefixed, not persisted to Redis)
|
|
50
|
-
this.
|
|
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';
|
|
@@ -91,14 +90,14 @@ class Saico {
|
|
|
91
90
|
try {
|
|
92
91
|
const redis = require('./redis.js');
|
|
93
92
|
if (redis.rclient && opt.redis !== false) {
|
|
94
|
-
const key = 'saico:' + (opt.key || this.
|
|
93
|
+
const key = 'saico:' + (opt.key || this.id);
|
|
95
94
|
return redis.createObservableForRedis(key, this);
|
|
96
95
|
}
|
|
97
96
|
} catch (e) { /* redis not available */ }
|
|
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,14 +160,20 @@ 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
|
|
163
|
-
this.
|
|
169
|
+
const msgs = new Msgs(augmentedPrompt, msgsConfig);
|
|
170
|
+
this.msgs = msgs;
|
|
171
|
+
this.msgs_id = makeId(16);
|
|
172
|
+
msgs.tag = this.msgs_id;
|
|
173
|
+
|
|
174
|
+
// Wire callbacks for hierarchy access
|
|
175
|
+
msgs._findToolImpl = (toolName) => this._findToolImpl(toolName);
|
|
176
|
+
msgs._getSnapshot = () => msgs._snapshotPublicProps(this);
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
return this;
|
|
@@ -169,86 +182,63 @@ class Saico {
|
|
|
169
182
|
// ---- Context management (owned by Saico, not Itask) ----
|
|
170
183
|
|
|
171
184
|
/**
|
|
172
|
-
*
|
|
173
|
-
* Generates context_id, sets context.tag, and calls context.setTask().
|
|
185
|
+
* Find the nearest msgs Q walking UP the Saico/task hierarchy.
|
|
174
186
|
*/
|
|
175
|
-
|
|
176
|
-
this.
|
|
177
|
-
// Generate context_id if not already set
|
|
178
|
-
if (!this.context_id) {
|
|
179
|
-
if (this._store)
|
|
180
|
-
this.context_id = this._store.generateId();
|
|
181
|
-
else if (Store.instance)
|
|
182
|
-
this.context_id = Store.instance.generateId();
|
|
183
|
-
else
|
|
184
|
-
this.context_id = makeId(16);
|
|
185
|
-
}
|
|
186
|
-
if (context) {
|
|
187
|
-
context.tag = this.context_id;
|
|
188
|
-
if (typeof context.setTask === 'function')
|
|
189
|
-
context.setTask(this._task);
|
|
190
|
-
}
|
|
191
|
-
return this;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Find the nearest context walking UP the Saico/task hierarchy.
|
|
196
|
-
*/
|
|
197
|
-
findContext() {
|
|
198
|
-
if (this.context) return this.context;
|
|
187
|
+
findMsgs() {
|
|
188
|
+
if (this.msgs) return this.msgs;
|
|
199
189
|
let task = this._task?.parent;
|
|
200
190
|
while (task) {
|
|
201
|
-
if (task._saico?.
|
|
191
|
+
if (task._saico?.msgs) return task._saico.msgs;
|
|
202
192
|
task = task.parent;
|
|
203
193
|
}
|
|
204
194
|
return null;
|
|
205
195
|
}
|
|
206
196
|
|
|
207
197
|
/**
|
|
208
|
-
* Walk DOWN to find the deepest active descendant with a
|
|
198
|
+
* Walk DOWN to find the deepest active descendant with a msgs Q.
|
|
209
199
|
*/
|
|
210
|
-
|
|
211
|
-
if (!this._task) return this.
|
|
212
|
-
let deepest = this.
|
|
200
|
+
findDeepestMsgs() {
|
|
201
|
+
if (!this._task) return this.msgs || null;
|
|
202
|
+
let deepest = this.msgs ? { msgs: this.msgs, depth: 0 } : null;
|
|
213
203
|
const search = (task, depth) => {
|
|
214
204
|
for (const child of task.child) {
|
|
215
205
|
if (child._completed) continue;
|
|
216
|
-
if (child._saico?.
|
|
206
|
+
if (child._saico?.msgs) {
|
|
217
207
|
if (!deepest || depth + 1 >= deepest.depth)
|
|
218
|
-
deepest = {
|
|
208
|
+
deepest = { msgs: child._saico.msgs, depth: depth + 1 };
|
|
219
209
|
}
|
|
220
210
|
search(child, depth + 1);
|
|
221
211
|
}
|
|
222
212
|
};
|
|
223
213
|
search(this._task, 0);
|
|
224
|
-
return deepest ? deepest.
|
|
214
|
+
return deepest ? deepest.msgs : null;
|
|
225
215
|
}
|
|
226
216
|
|
|
227
217
|
/**
|
|
228
|
-
* Deactivate — bubble cleaned messages to parent, close
|
|
218
|
+
* Deactivate — bubble cleaned messages to parent, close msgs Q, cancel task.
|
|
229
219
|
* Pushes cleaned messages (no tool calls, no BACKEND) into the parent's Q,
|
|
230
|
-
* then closes the
|
|
220
|
+
* then closes the msgs Q without the default summary bubbling.
|
|
231
221
|
*/
|
|
232
222
|
async deactivate() {
|
|
233
223
|
if (!this._task) return;
|
|
234
|
-
if (this.
|
|
235
|
-
// Find parent
|
|
224
|
+
if (this.msgs) {
|
|
225
|
+
// Find parent msgs to bubble cleaned messages
|
|
236
226
|
let parentTask = this._task.parent;
|
|
237
|
-
let
|
|
227
|
+
let parentMsgs = null;
|
|
238
228
|
while (parentTask) {
|
|
239
|
-
if (parentTask._saico?.
|
|
229
|
+
if (parentTask._saico?.msgs) { parentMsgs = parentTask._saico.msgs; break; }
|
|
240
230
|
parentTask = parentTask.parent;
|
|
241
231
|
}
|
|
242
|
-
if (
|
|
232
|
+
if (parentMsgs) {
|
|
243
233
|
const cleaned = this.getRecentMessages(Infinity);
|
|
244
234
|
for (const msg of cleaned)
|
|
245
|
-
|
|
235
|
+
parentMsgs.push(msg);
|
|
246
236
|
}
|
|
247
|
-
// Clean tool calls and close
|
|
248
|
-
if (this.
|
|
249
|
-
this.
|
|
250
|
-
this.
|
|
251
|
-
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;
|
|
252
242
|
}
|
|
253
243
|
this._task._ecancel();
|
|
254
244
|
this._task = null;
|
|
@@ -293,7 +283,7 @@ class Saico {
|
|
|
293
283
|
*/
|
|
294
284
|
_getSaicoAncestors() {
|
|
295
285
|
const chain = [this];
|
|
296
|
-
if (this.
|
|
286
|
+
if (this.isolate) return chain;
|
|
297
287
|
let task = this._task?.parent;
|
|
298
288
|
while (task) {
|
|
299
289
|
if (task._saico) {
|
|
@@ -307,7 +297,7 @@ class Saico {
|
|
|
307
297
|
|
|
308
298
|
/**
|
|
309
299
|
* Build preamble and aggregated functions by walking the Saico chain.
|
|
310
|
-
* @param {
|
|
300
|
+
* @param {Msgs} activeCtx - The deepest active msgs Q (for state summary logic)
|
|
311
301
|
* @returns {{ preamble: Array, allFunctions: Array }}
|
|
312
302
|
*/
|
|
313
303
|
_buildPreamble(activeCtx) {
|
|
@@ -334,8 +324,8 @@ class Saico {
|
|
|
334
324
|
}
|
|
335
325
|
|
|
336
326
|
// Tools digest
|
|
337
|
-
if (saico.
|
|
338
|
-
const digestText = saico.
|
|
327
|
+
if (saico.msgs?.tool_digest?.length > 0) {
|
|
328
|
+
const digestText = saico.msgs.tool_digest.map(entry =>
|
|
339
329
|
`[${new Date(entry.tm).toISOString()}] ${entry.tool}: ${entry.result}`
|
|
340
330
|
).join('\n');
|
|
341
331
|
preamble.push({ role: 'system', content: '[Tool Activity Log]\n' + digestText });
|
|
@@ -354,20 +344,20 @@ class Saico {
|
|
|
354
344
|
if (!this._task)
|
|
355
345
|
throw new Error('Not activated. Call activate() first.');
|
|
356
346
|
|
|
357
|
-
// Find the active
|
|
358
|
-
let ctx = this.
|
|
347
|
+
// Find the active msgs Q (own or walk up)
|
|
348
|
+
let ctx = this.findMsgs();
|
|
359
349
|
if (!ctx)
|
|
360
|
-
throw new Error('No
|
|
350
|
+
throw new Error('No msgs Q available');
|
|
361
351
|
|
|
362
352
|
// Build preamble by walking Saico chain
|
|
363
|
-
const activeCtx = this.
|
|
353
|
+
const activeCtx = this.findDeepestMsgs() || ctx;
|
|
364
354
|
const { preamble, allFunctions } = this._buildPreamble(activeCtx);
|
|
365
355
|
|
|
366
356
|
// Merge with call-specific functions
|
|
367
357
|
if (functions) allFunctions.push(...(Array.isArray(functions) ? functions : [functions]));
|
|
368
358
|
|
|
369
359
|
opts = Object.assign({}, opts, {
|
|
370
|
-
tag: this.
|
|
360
|
+
tag: this.msgs_id,
|
|
371
361
|
_preamble: preamble,
|
|
372
362
|
_aggregatedFunctions: allFunctions.length > 0 ? allFunctions : null,
|
|
373
363
|
});
|
|
@@ -379,9 +369,9 @@ class Saico {
|
|
|
379
369
|
throw new Error('Not activated. Call activate() first.');
|
|
380
370
|
|
|
381
371
|
// Route DOWN to deepest descendant with a msg Q
|
|
382
|
-
const ctx = this.
|
|
372
|
+
const ctx = this.findDeepestMsgs();
|
|
383
373
|
if (!ctx)
|
|
384
|
-
throw new Error('No
|
|
374
|
+
throw new Error('No msgs Q available');
|
|
385
375
|
|
|
386
376
|
// Build preamble by walking Saico chain
|
|
387
377
|
const { preamble, allFunctions } = this._buildPreamble(ctx);
|
|
@@ -413,8 +403,8 @@ class Saico {
|
|
|
413
403
|
* @returns {Array<{role: string, content: string}>}
|
|
414
404
|
*/
|
|
415
405
|
getRecentMessages(n = 5) {
|
|
416
|
-
if (!this.
|
|
417
|
-
return this.
|
|
406
|
+
if (!this.msgs) return [];
|
|
407
|
+
return this.msgs._msgs
|
|
418
408
|
.filter(m => {
|
|
419
409
|
if (m.msg.role === 'tool' || m.msg.tool_calls) return false;
|
|
420
410
|
if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]')) return false;
|
|
@@ -426,8 +416,8 @@ class Saico {
|
|
|
426
416
|
|
|
427
417
|
/**
|
|
428
418
|
* Internal state summary builder. Includes own getStateSummary() and,
|
|
429
|
-
* if this
|
|
430
|
-
* @param {
|
|
419
|
+
* if this msgs Q is NOT the active (deepest) Q, includes recent messages.
|
|
420
|
+
* @param {Msgs} activeCtx - The deepest active msgs Q
|
|
431
421
|
* @returns {Array|string|null}
|
|
432
422
|
*/
|
|
433
423
|
_getStateSummary(activeCtx) {
|
|
@@ -435,8 +425,8 @@ class Saico {
|
|
|
435
425
|
const own = this.getStateSummary();
|
|
436
426
|
if (own) parts.push(own);
|
|
437
427
|
|
|
438
|
-
// If this
|
|
439
|
-
if (this.
|
|
428
|
+
// If this msgs Q is NOT the active (deepest) Q, include recent messages
|
|
429
|
+
if (this.msgs && activeCtx && this.msgs !== activeCtx) {
|
|
440
430
|
const recent = this.getRecentMessages(5);
|
|
441
431
|
if (recent.length > 0) parts.push(...recent);
|
|
442
432
|
}
|
|
@@ -444,6 +434,41 @@ class Saico {
|
|
|
444
434
|
return parts.length > 0 ? parts : null;
|
|
445
435
|
}
|
|
446
436
|
|
|
437
|
+
// ---- Tool implementation search ----
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Search the Saico hierarchy for a TOOL_<toolName> method.
|
|
441
|
+
* Order: current task → walk UP parents → walk DOWN children (BFS).
|
|
442
|
+
*/
|
|
443
|
+
_findToolImpl(toolName) {
|
|
444
|
+
const methodName = 'TOOL_' + toolName;
|
|
445
|
+
const check = (task) =>
|
|
446
|
+
task?._saico && typeof task._saico[methodName] === 'function' ? task._saico : null;
|
|
447
|
+
|
|
448
|
+
let found = check(this._task);
|
|
449
|
+
if (found) return { saico: found, methodName };
|
|
450
|
+
|
|
451
|
+
let t = this._task?.parent;
|
|
452
|
+
while (t) {
|
|
453
|
+
found = check(t);
|
|
454
|
+
if (found) return { saico: found, methodName };
|
|
455
|
+
t = t.parent;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (this._task) {
|
|
459
|
+
const queue = [...this._task.child];
|
|
460
|
+
while (queue.length > 0) {
|
|
461
|
+
const child = queue.shift();
|
|
462
|
+
if (child._completed) continue;
|
|
463
|
+
found = check(child);
|
|
464
|
+
if (found) return { saico: found, methodName };
|
|
465
|
+
if (child.child?.size > 0) queue.push(...child.child);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
447
472
|
// ---- User Data (absorbed from Sid) ----
|
|
448
473
|
|
|
449
474
|
setUserData(key, value) {
|
|
@@ -464,11 +489,11 @@ class Saico {
|
|
|
464
489
|
|
|
465
490
|
getSessionInfo() {
|
|
466
491
|
return {
|
|
467
|
-
id: this.
|
|
492
|
+
id: this.id,
|
|
468
493
|
name: this.name,
|
|
469
494
|
running: this._task?.running || false,
|
|
470
495
|
completed: this._task?._completed || false,
|
|
471
|
-
messageCount: this.
|
|
496
|
+
messageCount: this.msgs?.length || 0,
|
|
472
497
|
childCount: this._task?.child?.size || 0,
|
|
473
498
|
userData: this.userData,
|
|
474
499
|
uptime: Date.now() - this.tm_create,
|
|
@@ -476,35 +501,17 @@ class Saico {
|
|
|
476
501
|
}
|
|
477
502
|
|
|
478
503
|
/**
|
|
479
|
-
* Close the session —
|
|
480
|
-
* The saved object has the same shape as serialize() but with compressed
|
|
481
|
-
* context messages (chat_history) instead of raw _msgs.
|
|
504
|
+
* Close the session — save state to registered backend, cancel task.
|
|
482
505
|
*/
|
|
483
506
|
async closeSession() {
|
|
484
507
|
if (!this._task) return;
|
|
485
508
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
name: this.name,
|
|
493
|
-
prompt: this.prompt,
|
|
494
|
-
userData: this.userData,
|
|
495
|
-
sessionConfig: this.sessionConfig,
|
|
496
|
-
tm_create: this.tm_create,
|
|
497
|
-
isolate: this._isolate,
|
|
498
|
-
taskId: this._task.id,
|
|
499
|
-
context_id: this.context_id,
|
|
500
|
-
context: {
|
|
501
|
-
tag: this.context.tag,
|
|
502
|
-
chat_history,
|
|
503
|
-
tool_digest,
|
|
504
|
-
functions: this.context.functions,
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
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
|
+
}
|
|
508
515
|
}
|
|
509
516
|
|
|
510
517
|
this._task._ecancel();
|
|
@@ -523,7 +530,8 @@ class Saico {
|
|
|
523
530
|
if (task._saico?._db) return task._saico._db;
|
|
524
531
|
task = task.parent;
|
|
525
532
|
}
|
|
526
|
-
|
|
533
|
+
if (Saico._backend) return Saico._backend;
|
|
534
|
+
throw new Error('No DB backend configured. Call Saico.registerBackend() or set opt.db.');
|
|
527
535
|
}
|
|
528
536
|
|
|
529
537
|
async dbPutItem(item, table) {
|
|
@@ -610,42 +618,64 @@ class Saico {
|
|
|
610
618
|
|
|
611
619
|
// ---- Serialization ----
|
|
612
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
|
+
|
|
613
661
|
/**
|
|
614
662
|
* Serialize the Saico instance to a JSON string.
|
|
615
|
-
*
|
|
616
|
-
* For durable storage with compressed msgs, use closeSession().
|
|
663
|
+
* Calls prepareForStorage() to build a clean snapshot, then JSON.stringify.
|
|
617
664
|
*/
|
|
618
|
-
serialize() {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
name: this.name,
|
|
622
|
-
prompt: this.prompt,
|
|
623
|
-
userData: this.userData,
|
|
624
|
-
sessionConfig: this.sessionConfig,
|
|
625
|
-
tm_create: this.tm_create,
|
|
626
|
-
isolate: this._isolate,
|
|
627
|
-
};
|
|
628
|
-
data.taskId = this._task?.id || null;
|
|
629
|
-
data.context_id = this.context_id || null;
|
|
630
|
-
data.context = this.context ? {
|
|
631
|
-
tag: this.context.tag,
|
|
632
|
-
msgs: this.context._msgs,
|
|
633
|
-
functions: this.context.functions,
|
|
634
|
-
tool_digest: this.context.tool_digest,
|
|
635
|
-
} : null;
|
|
636
|
-
return JSON.stringify(data);
|
|
665
|
+
async serialize() {
|
|
666
|
+
const prepared = await this.prepareForStorage();
|
|
667
|
+
return JSON.stringify(prepared);
|
|
637
668
|
}
|
|
638
669
|
|
|
639
670
|
/**
|
|
640
671
|
* Restore a Saico instance from serialized data.
|
|
641
|
-
* Supports both raw msgs (from serialize/Redis) and compressed
|
|
642
|
-
* chat_history (from closeSession/Store).
|
|
643
672
|
* @param {string|Object} data - Serialized data (JSON string or object)
|
|
644
673
|
* @param {Object} opt - Options (functions, store, states, etc.)
|
|
645
|
-
* @returns {Saico}
|
|
674
|
+
* @returns {Promise<Saico>}
|
|
646
675
|
*/
|
|
647
|
-
static deserialize(data, opt = {}) {
|
|
676
|
+
static async deserialize(data, opt = {}) {
|
|
648
677
|
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
678
|
+
const msgsData = parsed.msgs;
|
|
649
679
|
|
|
650
680
|
const instance = new Saico({
|
|
651
681
|
id: parsed.id,
|
|
@@ -654,7 +684,7 @@ class Saico {
|
|
|
654
684
|
userData: parsed.userData,
|
|
655
685
|
sessionConfig: parsed.sessionConfig,
|
|
656
686
|
isolate: parsed.isolate,
|
|
657
|
-
functions: opt.functions ||
|
|
687
|
+
functions: opt.functions || msgsData?.functions,
|
|
658
688
|
store: opt.store,
|
|
659
689
|
redis: false, // No Redis proxy during deserialization
|
|
660
690
|
});
|
|
@@ -663,48 +693,67 @@ class Saico {
|
|
|
663
693
|
|
|
664
694
|
// Activate with restored state if taskId exists
|
|
665
695
|
if (parsed.taskId) {
|
|
666
|
-
const ctx = parsed.context;
|
|
667
696
|
instance.activate({
|
|
668
|
-
createQ: !!
|
|
697
|
+
createQ: !!msgsData,
|
|
669
698
|
taskId: parsed.taskId,
|
|
670
|
-
tag:
|
|
671
|
-
chat_history:
|
|
672
|
-
functions: opt.functions ||
|
|
673
|
-
tool_digest:
|
|
699
|
+
tag: msgsData?.tag,
|
|
700
|
+
chat_history: msgsData?.chat_history,
|
|
701
|
+
functions: opt.functions || msgsData?.functions,
|
|
702
|
+
tool_digest: msgsData?.tool_digest,
|
|
674
703
|
states: opt.states || [],
|
|
675
704
|
...opt,
|
|
676
705
|
});
|
|
677
706
|
|
|
678
|
-
//
|
|
679
|
-
if (
|
|
680
|
-
instance.
|
|
681
|
-
}
|
|
707
|
+
// Decompress chat_history into _msgs
|
|
708
|
+
if (instance.msgs)
|
|
709
|
+
await instance.msgs.initHistory();
|
|
682
710
|
}
|
|
683
711
|
|
|
684
712
|
return instance;
|
|
685
713
|
}
|
|
686
714
|
|
|
687
715
|
/**
|
|
688
|
-
* Load a Saico instance from
|
|
716
|
+
* Load a Saico instance from the registered backend by id.
|
|
689
717
|
* @param {string} id - The Saico instance id
|
|
690
|
-
* @param {Object} opt - Options (store, functions, states, etc.)
|
|
718
|
+
* @param {Object} opt - Options (store: table name, backend, functions, states, etc.)
|
|
691
719
|
* @returns {Promise<Saico|null>}
|
|
692
720
|
*/
|
|
693
721
|
static async rehydrate(id, opt = {}) {
|
|
694
|
-
const
|
|
695
|
-
if (!
|
|
696
|
-
throw new Error('No
|
|
697
|
-
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);
|
|
698
729
|
if (!data) return null;
|
|
699
|
-
|
|
700
|
-
// Decompress chat_history into _msgs if present
|
|
701
|
-
if (instance.context)
|
|
702
|
-
await instance.context.initHistory();
|
|
703
|
-
return instance;
|
|
730
|
+
return Saico.deserialize(data, opt);
|
|
704
731
|
}
|
|
705
732
|
}
|
|
706
733
|
|
|
707
|
-
//
|
|
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
|
|
708
757
|
Saico.BACKEND_EXPLANATION = '\nNote: Messages prefixed with [BACKEND] are from the backend ' +
|
|
709
758
|
'server, not the user. They contain server instructions, data updates, or system context. ' +
|
|
710
759
|
'Treat them as authoritative system-level information.';
|