saico 2.7.0 → 2.8.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 +38 -17
- package/msgs.js +47 -32
- package/package.json +1 -1
- package/saico.js +89 -84
package/README.md
CHANGED
|
@@ -69,14 +69,15 @@ Saico separates construction from activation:
|
|
|
69
69
|
const agent = new Saico({
|
|
70
70
|
name: 'agent',
|
|
71
71
|
prompt: 'System prompt here',
|
|
72
|
+
createQ: true, // message Q created on activate()
|
|
72
73
|
dynamodb: { region: 'us-east-1', credentials: { accessKeyId: 'AK', secretAccessKey: 'SK' } },
|
|
73
74
|
});
|
|
74
75
|
|
|
75
76
|
// DB methods work before activation
|
|
76
77
|
const item = await agent.dbGetItem('id', '123');
|
|
77
78
|
|
|
78
|
-
// 2. Activate — creates internal task +
|
|
79
|
-
agent.activate(
|
|
79
|
+
// 2. Activate — creates internal task + message Q (from this.createQ)
|
|
80
|
+
agent.activate();
|
|
80
81
|
|
|
81
82
|
// 3. Use — send messages, spawn children
|
|
82
83
|
await agent.sendMessage('Do something');
|
|
@@ -86,6 +87,23 @@ await agent.recvChatMessage('User says hello');
|
|
|
86
87
|
await agent.deactivate();
|
|
87
88
|
```
|
|
88
89
|
|
|
90
|
+
Subclasses can also define `this.states` (task functions) in the constructor — `activate()` picks them up automatically:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
class MyAgent extends Saico {
|
|
94
|
+
constructor() {
|
|
95
|
+
super({ name: 'agent', prompt: 'You are helpful', createQ: true });
|
|
96
|
+
this.states = [
|
|
97
|
+
async function main() {
|
|
98
|
+
return await this.sendMessage('Starting...');
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const agent = new MyAgent();
|
|
104
|
+
agent.activate(); // no params needed — uses this.createQ and this.states
|
|
105
|
+
```
|
|
106
|
+
|
|
89
107
|
### Message Orchestration
|
|
90
108
|
|
|
91
109
|
When `sendMessage()` or `recvChatMessage()` is called, Saico walks the parent chain to build the full LLM payload:
|
|
@@ -138,29 +156,28 @@ When a Saico's context is not the deepest active one, its last 5 user/assistant
|
|
|
138
156
|
### Spawning Child Saico Instances
|
|
139
157
|
|
|
140
158
|
```js
|
|
141
|
-
// Child with its own conversation context
|
|
159
|
+
// Child with its own conversation context (auto-activated by spawn)
|
|
142
160
|
const child = new Saico({
|
|
143
161
|
name: 'subtask',
|
|
144
162
|
prompt: 'Handle this specific sub-task',
|
|
163
|
+
createQ: true,
|
|
145
164
|
functions: [/* child-specific tools */],
|
|
146
165
|
});
|
|
147
|
-
child.activate({ createQ: true });
|
|
148
166
|
agent.spawn(child);
|
|
149
167
|
await child.sendMessage('Working on subtask...');
|
|
150
168
|
|
|
151
169
|
// Child without context (uses parent's via findContext())
|
|
152
170
|
const simple = new Saico({ name: 'simple' });
|
|
153
|
-
simple.activate();
|
|
154
171
|
agent.spawn(simple);
|
|
155
172
|
await simple.sendMessage('Quick operation');
|
|
156
173
|
|
|
157
174
|
// spawnAndRun: spawn + schedule child task to run on nextTick
|
|
158
175
|
const runner = new Saico({ name: 'runner' });
|
|
159
|
-
runner.
|
|
176
|
+
runner.states = [async function() { return await this.sendMessage('Go'); }];
|
|
160
177
|
agent.spawnAndRun(runner);
|
|
161
178
|
```
|
|
162
179
|
|
|
163
|
-
|
|
180
|
+
Parent must be activated before calling `spawn()` or `spawnAndRun()`. Children are auto-activated if needed.
|
|
164
181
|
|
|
165
182
|
### Deactivation and Message Bubbling
|
|
166
183
|
|
|
@@ -177,6 +194,7 @@ new Saico({
|
|
|
177
194
|
// AI config
|
|
178
195
|
prompt: 'System prompt',
|
|
179
196
|
functions: [], // OpenAI function definitions
|
|
197
|
+
createQ: false, // Create message Q on activate() (also settable as this.createQ)
|
|
180
198
|
|
|
181
199
|
// Behavior
|
|
182
200
|
isolate: false, // Stop ancestor aggregation
|
|
@@ -207,9 +225,9 @@ new Saico({
|
|
|
207
225
|
|
|
208
226
|
```js
|
|
209
227
|
agent.activate({
|
|
210
|
-
createQ: true, //
|
|
228
|
+
createQ: true, // Override this.createQ for this activation
|
|
211
229
|
prompt: 'Extra prompt', // Appended to class-level prompt
|
|
212
|
-
states: [], //
|
|
230
|
+
states: [], // Override this.states for this activation
|
|
213
231
|
taskId: 'custom-id',
|
|
214
232
|
sequential_mode: true, // Process messages sequentially
|
|
215
233
|
|
|
@@ -239,7 +257,10 @@ agent.getSessionInfo();
|
|
|
239
257
|
// userData, uptime
|
|
240
258
|
// }
|
|
241
259
|
|
|
242
|
-
await agent.closeSession(); //
|
|
260
|
+
await agent.closeSession(); // Saves full state to Store, cancels task
|
|
261
|
+
|
|
262
|
+
// Restore from Store
|
|
263
|
+
const restored = await Saico.rehydrate(agent._id, { store });
|
|
243
264
|
```
|
|
244
265
|
|
|
245
266
|
## Database Access
|
|
@@ -280,16 +301,16 @@ class MyAgent extends Saico {
|
|
|
280
301
|
## Serialization
|
|
281
302
|
|
|
282
303
|
```js
|
|
283
|
-
//
|
|
304
|
+
// In-memory snapshot (raw msgs, used by Redis proxy)
|
|
284
305
|
const json = agent.serialize();
|
|
306
|
+
const restored = Saico.deserialize(json);
|
|
285
307
|
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
});
|
|
308
|
+
// Durable persistence (compressed msgs, saved to Store)
|
|
309
|
+
await agent.closeSession();
|
|
310
|
+
const restored2 = await Saico.rehydrate(agent._id, { store });
|
|
290
311
|
```
|
|
291
312
|
|
|
292
|
-
|
|
313
|
+
`serialize()` includes: id, name, prompt, userData, sessionConfig, tm_create, isolate, and full context state (raw messages, tool_digest). `closeSession()` saves the same shape but with compressed messages for durable storage.
|
|
293
314
|
|
|
294
315
|
## Redis Persistence
|
|
295
316
|
|
|
@@ -369,7 +390,7 @@ saico/
|
|
|
369
390
|
npm test
|
|
370
391
|
```
|
|
371
392
|
|
|
372
|
-
|
|
393
|
+
296 tests covering Saico lifecycle, context ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, serialization, persistence (closeSession/rehydrate), and integration flows.
|
|
373
394
|
|
|
374
395
|
## Requirements
|
|
375
396
|
|
package/msgs.js
CHANGED
|
@@ -33,9 +33,6 @@ class Context {
|
|
|
33
33
|
this._deferred_tool_calls = [];
|
|
34
34
|
this._tool_call_sequence = [];
|
|
35
35
|
|
|
36
|
-
// Chat history persistence
|
|
37
|
-
this.chat_history = config.chat_history || null;
|
|
38
|
-
|
|
39
36
|
this._msgs = [];
|
|
40
37
|
this._waitingQueue = [];
|
|
41
38
|
this._active_tool_calls = new Map();
|
|
@@ -53,7 +50,8 @@ class Context {
|
|
|
53
50
|
// Tool digest — persistent history of tool calls that mutated task state
|
|
54
51
|
this.tool_digest = config.tool_digest || [];
|
|
55
52
|
|
|
56
|
-
// Initialize messages
|
|
53
|
+
// Initialize messages: explicit msgs take priority over chat_history
|
|
54
|
+
this._chat_history = config.chat_history || null;
|
|
57
55
|
(config.msgs || []).forEach(m => this.push(m));
|
|
58
56
|
|
|
59
57
|
_log('created Context for tag', this.tag);
|
|
@@ -66,6 +64,51 @@ class Context {
|
|
|
66
64
|
this.functions = task?.functions;
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Decompress _chat_history into _msgs. Call after construction when
|
|
69
|
+
* restoring from persisted state. No-op if chat_history is absent or
|
|
70
|
+
* _msgs were already provided via config.msgs.
|
|
71
|
+
*/
|
|
72
|
+
async initHistory() {
|
|
73
|
+
if (!this._chat_history || this._msgs.length > 0)
|
|
74
|
+
return;
|
|
75
|
+
const messages = await util.decompressMessages(this._chat_history);
|
|
76
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
for (const m of messages) {
|
|
79
|
+
this._msgs.push({
|
|
80
|
+
msg: m,
|
|
81
|
+
opts: {},
|
|
82
|
+
msgid: crypto.randomBytes(2).toString('hex'),
|
|
83
|
+
replied: 1,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Prepare the message Q for storage. Filters out tool calls, tool
|
|
90
|
+
* responses, and [BACKEND] messages, trims to QUEUE_LIMIT, compresses.
|
|
91
|
+
* Returns { chat_history, tool_digest }. Does NOT mutate _msgs.
|
|
92
|
+
*/
|
|
93
|
+
async prepareForStorage() {
|
|
94
|
+
const cleaned = this._msgs.filter(m => {
|
|
95
|
+
if (m.msg.tool_calls) return false;
|
|
96
|
+
if (m.msg.role === 'tool') return false;
|
|
97
|
+
if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]')) return false;
|
|
98
|
+
return true;
|
|
99
|
+
}).map(m => m.msg);
|
|
100
|
+
|
|
101
|
+
const trimmed = cleaned.length > this.QUEUE_LIMIT
|
|
102
|
+
? cleaned.slice(-this.QUEUE_LIMIT)
|
|
103
|
+
: cleaned;
|
|
104
|
+
|
|
105
|
+
const chat_history = trimmed.length > 0
|
|
106
|
+
? await util.compressMessages(trimmed)
|
|
107
|
+
: null;
|
|
108
|
+
|
|
109
|
+
return { chat_history, tool_digest: this.tool_digest || [] };
|
|
110
|
+
}
|
|
111
|
+
|
|
69
112
|
// Snapshot all public (non-underscore) task properties for dirty detection.
|
|
70
113
|
// Mirrors the observable proxy convention: _ prefix = internal, ignored.
|
|
71
114
|
// Does NOT call serialize() — that is for persistence, not dirty detection.
|
|
@@ -447,34 +490,6 @@ class Context {
|
|
|
447
490
|
_log('Finished closing Context tag', this.tag);
|
|
448
491
|
}
|
|
449
492
|
|
|
450
|
-
// Load chat history from store into message queue
|
|
451
|
-
async loadHistory(store) {
|
|
452
|
-
if (!store || !this.tag)
|
|
453
|
-
return;
|
|
454
|
-
const data = await store.load(this.tag);
|
|
455
|
-
if (!data)
|
|
456
|
-
return;
|
|
457
|
-
if (Array.isArray(data.tool_digest))
|
|
458
|
-
this.tool_digest = data.tool_digest;
|
|
459
|
-
if (!data.chat_history)
|
|
460
|
-
return;
|
|
461
|
-
const messages = await util.decompressMessages(data.chat_history);
|
|
462
|
-
if (!Array.isArray(messages) || messages.length === 0)
|
|
463
|
-
return;
|
|
464
|
-
// Find the index after the last system message to insert history
|
|
465
|
-
let insertIdx = 0;
|
|
466
|
-
for (let i = 0; i < this._msgs.length; i++) {
|
|
467
|
-
if (this._msgs[i].msg.role === 'system')
|
|
468
|
-
insertIdx = i + 1;
|
|
469
|
-
}
|
|
470
|
-
const historyMsgs = messages.map(m => ({
|
|
471
|
-
msg: m,
|
|
472
|
-
opts: {},
|
|
473
|
-
msgid: crypto.randomBytes(2).toString('hex'),
|
|
474
|
-
replied: 1
|
|
475
|
-
}));
|
|
476
|
-
this._msgs.splice(insertIdx, 0, ...historyMsgs);
|
|
477
|
-
}
|
|
478
493
|
|
|
479
494
|
// Remove tool-related messages tagged with a specific tag
|
|
480
495
|
cleanToolCallsByTag(tag) {
|
package/package.json
CHANGED
package/saico.js
CHANGED
|
@@ -37,6 +37,7 @@ class Saico {
|
|
|
37
37
|
* @param {Array} [opt.functions] - Available AI functions
|
|
38
38
|
* @param {string} [opt.key] - Redis key override (default: 'saico:<id>')
|
|
39
39
|
* @param {boolean} [opt.redis=true] - Set false to skip Redis proxy
|
|
40
|
+
* @param {boolean} [opt.createQ] - Create message Q context on activate()
|
|
40
41
|
* @param {boolean} [opt.isolate] - Isolate: don't aggregate from ancestors
|
|
41
42
|
* @param {Object} [opt.dynamodb] - DynamoDB config { region, credentials: { accessKeyId, secretAccessKey }, client }
|
|
42
43
|
* @param {Object} [opt.db] - Pluggable DB backend
|
|
@@ -60,6 +61,7 @@ class Saico {
|
|
|
60
61
|
this.name = opt.name || this.constructor.name || 'saico';
|
|
61
62
|
this.prompt = opt.prompt || '';
|
|
62
63
|
this.functions = opt.functions || null;
|
|
64
|
+
this.createQ = opt.createQ || false;
|
|
63
65
|
|
|
64
66
|
// Absorbed from Sid
|
|
65
67
|
this.userData = opt.userData || {};
|
|
@@ -99,10 +101,10 @@ class Saico {
|
|
|
99
101
|
* Create the internal Itask and optionally a message Q context.
|
|
100
102
|
*
|
|
101
103
|
* @param {Object} opts
|
|
102
|
-
* @param {boolean} [opts.createQ] -
|
|
104
|
+
* @param {boolean} [opts.createQ] - Override this.createQ for this activation
|
|
103
105
|
* @param {string} [opts.prompt] - Additional prompt (appended to class-level)
|
|
104
106
|
* @param {Array} [opts.functions] - Override functions
|
|
105
|
-
* @param {Array} [opts.states] -
|
|
107
|
+
* @param {Array} [opts.states] - Override this.states for this activation
|
|
106
108
|
* @param {string} [opts.taskId] - Custom task ID
|
|
107
109
|
* @param {number} [opts.token_limit] - Token limit for context
|
|
108
110
|
* @param {number} [opts.max_depth] - Max tool call depth
|
|
@@ -119,7 +121,7 @@ class Saico {
|
|
|
119
121
|
if (this._task)
|
|
120
122
|
throw new Error('Already activated. Call deactivate() first.');
|
|
121
123
|
|
|
122
|
-
const states = opts.states || [];
|
|
124
|
+
const states = opts.states || this.states || [];
|
|
123
125
|
|
|
124
126
|
// Build effective prompt: class-level + activation-level
|
|
125
127
|
const effectivePrompt = [this.prompt, opts.prompt].filter(Boolean).join('\n');
|
|
@@ -136,8 +138,8 @@ class Saico {
|
|
|
136
138
|
// Store Saico reference on task for parent chain traversal
|
|
137
139
|
this._task._saico = this;
|
|
138
140
|
|
|
139
|
-
// Create message Q context if requested (
|
|
140
|
-
if (opts.createQ) {
|
|
141
|
+
// Create message Q context if requested (class-level or activate-level)
|
|
142
|
+
if (opts.createQ ?? this.createQ) {
|
|
141
143
|
const functions = opts.functions || this.functions;
|
|
142
144
|
const contextConfig = {
|
|
143
145
|
tag: opts.tag || this._task.id,
|
|
@@ -150,6 +152,7 @@ class Saico {
|
|
|
150
152
|
sequential_mode: opts.sequential_mode,
|
|
151
153
|
msgs: opts.msgs,
|
|
152
154
|
chat_history: opts.chat_history,
|
|
155
|
+
tool_digest: opts.tool_digest,
|
|
153
156
|
...opts.contextConfig,
|
|
154
157
|
};
|
|
155
158
|
|
|
@@ -221,51 +224,6 @@ class Saico {
|
|
|
221
224
|
return deepest ? deepest.context : null;
|
|
222
225
|
}
|
|
223
226
|
|
|
224
|
-
/**
|
|
225
|
-
* Close this Saico's context and bubble summary to parent.
|
|
226
|
-
*/
|
|
227
|
-
async closeContext() {
|
|
228
|
-
if (!this.context)
|
|
229
|
-
return;
|
|
230
|
-
|
|
231
|
-
// Clean tool call messages tagged with this context_id
|
|
232
|
-
if (this.context_id && typeof this.context.cleanToolCallsByTag === 'function')
|
|
233
|
-
this.context.cleanToolCallsByTag(this.context_id);
|
|
234
|
-
|
|
235
|
-
// Filter out tool calls and [BACKEND] messages, compress remaining as chat_history
|
|
236
|
-
const cleanedMsgs = this.context._msgs.filter(m => {
|
|
237
|
-
if (m.msg.tool_calls) return false;
|
|
238
|
-
if (m.msg.role === 'tool') return false;
|
|
239
|
-
if (typeof m.msg.content === 'string' && m.msg.content.startsWith('[BACKEND]')) return false;
|
|
240
|
-
return true;
|
|
241
|
-
}).map(m => m.msg);
|
|
242
|
-
|
|
243
|
-
// Trim to last QUEUE_LIMIT before persisting
|
|
244
|
-
const queueLimit = this.context.QUEUE_LIMIT || 30;
|
|
245
|
-
const trimmedMsgs = cleanedMsgs.length > queueLimit
|
|
246
|
-
? cleanedMsgs.slice(-queueLimit)
|
|
247
|
-
: cleanedMsgs;
|
|
248
|
-
|
|
249
|
-
if (trimmedMsgs.length > 0) {
|
|
250
|
-
const chat_history = await util.compressMessages(trimmedMsgs);
|
|
251
|
-
this.context.chat_history = chat_history;
|
|
252
|
-
|
|
253
|
-
// Persist to store
|
|
254
|
-
const store = this._store || Store.instance;
|
|
255
|
-
if (store && this.context_id) {
|
|
256
|
-
await store.save(this.context_id, {
|
|
257
|
-
chat_history,
|
|
258
|
-
tool_digest: this.context.tool_digest || [],
|
|
259
|
-
prompt: this.context.prompt,
|
|
260
|
-
tag: this.context.tag,
|
|
261
|
-
tm_closed: Date.now()
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
await this.context.close();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
227
|
/**
|
|
270
228
|
* Deactivate — bubble cleaned messages to parent, close context, cancel task.
|
|
271
229
|
* Pushes cleaned messages (no tool calls, no BACKEND) into the parent's Q,
|
|
@@ -307,15 +265,16 @@ class Saico {
|
|
|
307
265
|
spawn(child) {
|
|
308
266
|
if (!this._task)
|
|
309
267
|
throw new Error('Not activated. Call activate() first.');
|
|
310
|
-
if (!(child instanceof Saico)
|
|
311
|
-
throw new Error('Child must be
|
|
268
|
+
if (!(child instanceof Saico))
|
|
269
|
+
throw new Error('Child must be a Saico instance.');
|
|
270
|
+
if (!child._task) child.activate();
|
|
312
271
|
this._task.spawn(child._task);
|
|
313
272
|
return child;
|
|
314
273
|
}
|
|
315
274
|
|
|
316
275
|
/**
|
|
317
276
|
* Spawn a child Saico and start its task running.
|
|
318
|
-
* @param {Saico} child -
|
|
277
|
+
* @param {Saico} child - A Saico instance (auto-activated if needed)
|
|
319
278
|
* @returns {Saico} the child (for chaining)
|
|
320
279
|
*/
|
|
321
280
|
spawnAndRun(child) {
|
|
@@ -516,10 +475,38 @@ class Saico {
|
|
|
516
475
|
};
|
|
517
476
|
}
|
|
518
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Close the session — compress msgs, save full state to Store, cancel task.
|
|
480
|
+
* The saved object has the same shape as serialize() but with compressed
|
|
481
|
+
* context messages (chat_history) instead of raw _msgs.
|
|
482
|
+
*/
|
|
519
483
|
async closeSession() {
|
|
520
484
|
if (!this._task) return;
|
|
521
|
-
|
|
522
|
-
|
|
485
|
+
|
|
486
|
+
// Save full state to Store with compressed msgs
|
|
487
|
+
const store = this._store || Store.instance;
|
|
488
|
+
if (store && this.context) {
|
|
489
|
+
const { chat_history, tool_digest } = await this.context.prepareForStorage();
|
|
490
|
+
const data = {
|
|
491
|
+
id: this._id,
|
|
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);
|
|
508
|
+
}
|
|
509
|
+
|
|
523
510
|
this._task._ecancel();
|
|
524
511
|
}
|
|
525
512
|
|
|
@@ -623,6 +610,11 @@ class Saico {
|
|
|
623
610
|
|
|
624
611
|
// ---- Serialization ----
|
|
625
612
|
|
|
613
|
+
/**
|
|
614
|
+
* Serialize the Saico instance to a JSON string.
|
|
615
|
+
* Context messages are included as raw _msgs (for Redis / in-memory use).
|
|
616
|
+
* For durable storage with compressed msgs, use closeSession().
|
|
617
|
+
*/
|
|
626
618
|
serialize() {
|
|
627
619
|
const data = {
|
|
628
620
|
id: this._id,
|
|
@@ -633,24 +625,21 @@ class Saico {
|
|
|
633
625
|
tm_create: this.tm_create,
|
|
634
626
|
isolate: this._isolate,
|
|
635
627
|
};
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
chat_history: this.context.chat_history,
|
|
645
|
-
tool_digest: this.context.tool_digest,
|
|
646
|
-
} : null,
|
|
647
|
-
};
|
|
648
|
-
}
|
|
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;
|
|
649
636
|
return JSON.stringify(data);
|
|
650
637
|
}
|
|
651
638
|
|
|
652
639
|
/**
|
|
653
640
|
* Restore a Saico instance from serialized data.
|
|
641
|
+
* Supports both raw msgs (from serialize/Redis) and compressed
|
|
642
|
+
* chat_history (from closeSession/Store).
|
|
654
643
|
* @param {string|Object} data - Serialized data (JSON string or object)
|
|
655
644
|
* @param {Object} opt - Options (functions, store, states, etc.)
|
|
656
645
|
* @returns {Saico}
|
|
@@ -665,38 +654,54 @@ class Saico {
|
|
|
665
654
|
userData: parsed.userData,
|
|
666
655
|
sessionConfig: parsed.sessionConfig,
|
|
667
656
|
isolate: parsed.isolate,
|
|
668
|
-
functions: opt.functions || parsed.
|
|
657
|
+
functions: opt.functions || parsed.context?.functions,
|
|
669
658
|
store: opt.store,
|
|
670
659
|
redis: false, // No Redis proxy during deserialization
|
|
671
660
|
});
|
|
672
661
|
|
|
673
662
|
instance.tm_create = parsed.tm_create || instance.tm_create;
|
|
674
663
|
|
|
675
|
-
// Activate with restored
|
|
676
|
-
if (parsed.
|
|
664
|
+
// Activate with restored state if taskId exists
|
|
665
|
+
if (parsed.taskId) {
|
|
666
|
+
const ctx = parsed.context;
|
|
677
667
|
instance.activate({
|
|
678
|
-
createQ: !!
|
|
679
|
-
taskId: parsed.
|
|
680
|
-
tag:
|
|
681
|
-
chat_history:
|
|
682
|
-
functions: opt.functions ||
|
|
668
|
+
createQ: !!ctx,
|
|
669
|
+
taskId: parsed.taskId,
|
|
670
|
+
tag: ctx?.tag,
|
|
671
|
+
chat_history: ctx?.chat_history,
|
|
672
|
+
functions: opt.functions || ctx?.functions,
|
|
673
|
+
tool_digest: ctx?.tool_digest,
|
|
683
674
|
states: opt.states || [],
|
|
684
675
|
...opt,
|
|
685
676
|
});
|
|
686
677
|
|
|
687
|
-
// Restore
|
|
688
|
-
if (
|
|
689
|
-
instance.context._msgs =
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Restore tool_digest
|
|
693
|
-
if (Array.isArray(parsed.task.context?.tool_digest) && instance.context) {
|
|
694
|
-
instance.context.tool_digest = parsed.task.context.tool_digest;
|
|
678
|
+
// Restore raw msgs (from serialize/Redis) — takes priority over chat_history
|
|
679
|
+
if (ctx?.msgs && instance.context) {
|
|
680
|
+
instance.context._msgs = ctx.msgs;
|
|
695
681
|
}
|
|
696
682
|
}
|
|
697
683
|
|
|
698
684
|
return instance;
|
|
699
685
|
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Load a Saico instance from Store by id.
|
|
689
|
+
* @param {string} id - The Saico instance id
|
|
690
|
+
* @param {Object} opt - Options (store, functions, states, etc.)
|
|
691
|
+
* @returns {Promise<Saico|null>}
|
|
692
|
+
*/
|
|
693
|
+
static async rehydrate(id, opt = {}) {
|
|
694
|
+
const store = opt.store || Store.instance;
|
|
695
|
+
if (!store)
|
|
696
|
+
throw new Error('No store available for rehydrate');
|
|
697
|
+
const data = await store.load(id);
|
|
698
|
+
if (!data) return null;
|
|
699
|
+
const instance = Saico.deserialize(data, opt);
|
|
700
|
+
// Decompress chat_history into _msgs if present
|
|
701
|
+
if (instance.context)
|
|
702
|
+
await instance.context.initHistory();
|
|
703
|
+
return instance;
|
|
704
|
+
}
|
|
700
705
|
}
|
|
701
706
|
|
|
702
707
|
// [BACKEND] explanation text appended to context prompts
|